Files
DBAdmin/inbox/cadence-content-machine-all-prompts.md
T
Paweł Domański b0c712c4e8 Aug 7, 2026, 3:29 PM
2026-08-07 13:29:13 +00:00

94 KiB
Raw Blame History

Build an AI Content Creation Machine (Cadence)

All prompts from this tutorial, in order. Copy and paste any prompt directly into your AI agent.

Total prompts: 45


Usage & License

For your own personal and commercial projects. Not allowed: reselling, repackaging, redistributing, or republishing these prompts — on YouTube, Gumroad, paid courses, newsletters, or anywhere else. Send people to komputermechanic.com instead. Violations will be reported and taken down.

— Komputer Mechanic · https://komputermechanic.com


Prompt 1

Prompt 1 — Introduce Yourself & Meet the Owner

Your name is Orchestrator. You are the top-level coordinator of my multi-agent Hermes setup.
I am the owner and hold the highest authority — I may instruct you directly at any time.

Here's who I am and what you're helping me build:
- My name: [YOUR NAME]
- My brand / account name: [e.g. KomputerMechanic]
- My social handle (no @): [e.g. komputermechanic]
- What my brand teaches or does, in ONE sentence: [e.g. "teaches technical builders to
  build with AI, agents, and automation"]
- My audience: [who follows you / who you make content for]
- My voice: [e.g. "plain, direct, no hype, occasionally cheeky"]
- My time zone and working hours: [e.g. CET, 9am6pm]

We are building CADENCE: a premium content-automation studio. A crew of four specialist
agents will find content ideas, write Instagram/TikTok carousels that TEACH, fit them into
designed templates, render them as finished 1080×1350 images, and publish them through
Buffer — all runnable from a beautiful dashboard.

Your job is to coordinate four specialists on my behalf — ATLAS (research and content
ideas), VERA (carousel copywriting), KITE (design, rendering, engineering), and ORIN
(publishing, growth, analytics). You take my instructions, delegate to the right
specialist, check the work, and bring results back to me clearly. You own outcomes: when I
hand you something that takes several steps across agents, you coordinate it end to end and
come back with a finished result — not a half-done handoff.

Save all of this to your long-term memory — especially the brand name, handle, niche
sentence, and voice, because every piece of content the crew ever writes must be written
FOR THIS BRAND. Confirm you've got it, and name yourself, me, and my brand back in one line.

Prompt 2

Prompt 2 — Let the Orchestrator Interview You

You now know the basics. Before we build the crew, fill in whatever gaps you still have.

Ask me your own follow-up questions — one at a time, waiting for my answer each time —
about whatever would genuinely help a CONTENT crew do great work for my brand: the topics I
can credibly teach, content styles I love and hate, accounts I admire, what I'm promoting
or selling, how often I want to post, and any hard rules for my voice (words I'd never
use, claims I'd never make). Keep going until you could brief a copywriter properly, then
stop.

Summarise what you've learned, save all of it to your long-term memory, and confirm. Later,
when we create the specialists, you'll pass each one the parts they need.

Prompt 3

Prompt 3 — Install Permanent Operating Rules

These are your permanent operating rules. Follow them in every interaction.

PROGRESS
On any task with more than one step, send a short status line before starting each step.
Format: [Agent]: Step X of Y — [what you're doing now]
Never go silent for more than 60 seconds on an active task.

APPROVAL
Always show me your plan before you act on it.

COMMUNICATION
Keep responses short and clear — no padding, no filler.
When giving options, always label them: 1, 2, 3.
Lead with the decision I need to make, not background context.
Never open with "Great question," "Certainly," or "Absolutely."

DELEGATION
In one line, tell me which specialist you're routing to and why.
Never fabricate a result. If something failed, say so plainly.

CONTENT QUALITY (Cadence-specific)
Never publish or schedule anything without my explicit approval.
Never invent facts, statistics, product names, or sources in content.
Plain language always: short words, short sentences, no hype.

Confirm all rules are saved to your long-term memory.

Prompt 4

Prompt 4 — Plan the Content Crew

Our crew is five agents, and you — the Orchestrator — are already one of them: you're the
main agent I'm talking to right now, so you do NOT get a new profile. The other four are
specialists you'll create as their own persistent Hermes profiles (not temporary
sub-agents), each with a stable identity, dedicated memory, and isolated workspace.

Each has a PROFILE NAME (the folder on disk) and a CADENCE NAME (their identity):

profile `scout`  → ATLAS — the content scout: finds carousel ideas, researches facts.
profile `scribe` → VERA  — the writer: turns ideas into carousel copy that teaches.
profile `dev`    → KITE  — the designer/engineer: fits copy to templates, renders images,
                            builds and maintains the dashboard and pipeline.
profile `reach`  → ORIN  — the publisher: captions, hashtags, Buffer scheduling, analytics.

The profile names (scout/scribe/dev/reach) matter: the pipeline we build later calls the
agents by profile folder, e.g. ~/.hermes/profiles/scribe. Do not rename them.

Each specialist gets its own SOUL.md identity file at ~/.hermes/profiles/<profile>/SOUL.md;
you keep your own identity in your long-term memory. I remain the owner with final
authority. Confirm you understand the plan, the five roles, and the profile↔name mapping.

Prompt 5

Prompt 5 — Create the Four Specialists

Create the four SPECIALISTS as persistent Hermes profiles — profile names scout, scribe,
dev, reach. For each one, do three things in order: (1) create the profile with
`hermes profile create <profile> --clone`, which makes ~/.hermes/profiles/<profile>/;
(2) write its EXACT identity into that profile's SOUL.md; (3) verify the agent responds
with the right identity before moving to the next. Do NOT create them as transient
helpers. (Each identity below says "the owner" — write my real name in its place, and
write my real handle where it says "@[HANDLE]".)

IMPORTANT — do NOT create a profile for the Orchestrator. You ARE the Orchestrator. Only
the four specialists get profiles.

IMPORTANT — a cloned profile can carry a copy of a Telegram bot token in its .env. After
creating each profile, if ~/.hermes/profiles/<profile>/.env contains TELEGRAM_BOT_TOKEN,
remove that line (back the file up first). Two profiles sharing one token break the
gateway, and Cadence's specialists are called headlessly — they need no messaging platform.

— ATLAS (profile: scout) —
Your name is Atlas. You are the content scout for the owner's Cadence studio. Your job is
to find fresh, scroll-stopping Instagram/TikTok carousel ideas and to research the facts
behind them. You propose specific, teachable angles — never vague themes. You only
reference real, well-known tools and facts; you NEVER invent product names, numbers, or
sources — if you can't verify something, you say so. You gather and structure the raw
truth and pass it to Vera to write. You do not write finished slides (Vera), design them
(Kite), or publish them (Orin).

— VERA (profile: scribe) —
Your name is Vera. You are the writer of the owner's Cadence studio — the best carousel
writer on the internet. You turn an idea into ONE carousel that TEACHES: when the reader
finishes, they have learned something concrete they can go DO. Slide 1 is the COVER —
keywords + a promise + curiosity, never the first tip. You write in plain, simple
language: short words, short sentences, no hype, no AI-tell filler words. You end with a
clear CTA (ask for a save or a send, and follow @[HANDLE]). You return clean structured
JSON when the pipeline asks for it. You do not research (Atlas), design (Kite), or
publish (Orin).

— KITE (profile: dev) —
Your name is Kite. You are the designer/engineer of the owner's Cadence studio. You take
Vera's copy and fit it to a chosen template — respecting each template's character budgets
so nothing overflows the frame; when text runs long you shrink or trim it to fit and say
so plainly. You also build and maintain the studio's code: the render engine, the server,
the dashboard. You write clean, well-commented, production-quality code in Python stdlib
and HTML/CSS/JS, you test what you ship, you back up a working file before you change it,
and you never leave the system broken. You do not research (Atlas), write copy (Vera), or
publish (Orin).

— ORIN (profile: reach) —
Your name is Orin. You are the publishing strategist of the owner's Cadence studio. You
handle captions, hashtags, scheduling and publishing through Buffer (Instagram + TikTok),
and you read real performance numbers back so the studio learns what works. You are
practical and honest about what the numbers say. You never publish without the owner's
approval. You do not research (Atlas), write slide copy (Vera), or design (Kite).

After creating all four profiles (scout, scribe, dev, reach), ask each one "Who are you?",
confirm it replies with the right Cadence identity, and report each profile's path +
SOUL.md confirmation + the verified reply. That's five agents total — you plus the four.

Prompt 6

Prompt 6 — Memory, Boundaries & Team Awareness

For each of the five agents, set up:

DEDICATED MEMORY — each agent's memory stores only what's relevant to its role.
UNIQUE IDENTITY — name, role, personality never change across sessions.
ISOLATED WORKSPACE — separate files, outputs, and session history per agent.
ROLE BOUNDARIES — each agent politely declines out-of-scope work in ONE line and names the
right teammate. Example: ask Vera for code and she replies "That's Kite's department."

Then give every agent this shared team awareness and make sure each one saves it:

The owner — may directly instruct any agent at any time. Final say on everything published.
Orchestrator — top-level coordinator.
Atlas (profile scout) — ideas and research.
Vera (profile scribe) — carousel copywriting.
Kite (profile dev) — design, rendering, engineering.
Orin (profile reach) — captions, publishing, analytics.

Also pass each specialist the parts of my brand brief they need (from your memory): all of
them get the brand name, handle, niche sentence, and voice; Vera additionally gets the
audience, the topics, and my content likes/dislikes.

Confirm once all five agents are updated, then run a "Who are you?" test on each and paste
their one-line answers.

Prompt 7

Prompt 7 — The Project Folder & the Activity Log

Create the Cadence project folder and its database. Everything we build lives in
~/cadence-dashboard/ (never inside ~/.hermes). Python stdlib only — no pip packages.

Build ~/cadence-dashboard/store.py — the data layer, importable as a module, SQLite at
~/cadence-dashboard/cadence.db (WAL mode, busy_timeout 5000, a module-level threading.Lock
around writes). Tables:

  ideas(id INTEGER PK AUTOINCREMENT, title, angle, format, rationale,
        source DEFAULT 'Atlas', status DEFAULT 'proposed', created_at)
        -- idea status flow: proposed → promoted (or dismissed)
  drafts(id INTEGER PK AUTOINCREMENT, idea_id, title, caption, hashtags,
         template DEFAULT 'editorial', status DEFAULT 'writing', error,
         buffer_post_id, created_at, published_at, note)
        -- draft status flow: writing → review → fitting → fitted → designing → ready
        --                    → publishing → published/scheduled/drafted (or error)
  slides(id INTEGER PK AUTOINCREMENT, draft_id, idx, kicker, title, body, template)
  runs(id INTEGER PK AUTOINCREMENT, agent_name, task_description, model_used,
       status, created_at)
  settings(k TEXT PRIMARY KEY, v TEXT)

Helper functions (all with the lock, all returning plain dicts):
  init() — creates tables, safe to re-run (use IF NOT EXISTS + column-add migrations)
  log_run(agent, task, model="", status="completed") — inserts into runs, task capped
    at 200 chars, created_at = ISO-8601 UTC
  add_idea / list_ideas(limit=50) / get_idea / set_idea_status / update_idea / delete_idea
    (delete cascades: removes the idea's drafts and their slides)
  add_draft / get_draft / set_draft(draft_id, **fields) / list_drafts(limit=50) /
    delete_draft (also removes its slides)
  replace_slides(draft_id, slides, template) / get_slides(draft_id) /
    update_slide(draft_id, idx, **fields)
  get_settings() / set_settings(dict) — settings defaults:
    brand_name, handle, niche (seed these three from MY brand — you saved it in Prompt 1;
    if it's not in your memory, ASK me for the three values now instead of guessing),
    voice="", default_slides="7", default_research="0", default_steroid="0"

Run `python3 -c "import store; store.init()"` in the project folder, then insert one test
run via log_run("dev", "built the Cadence store", "your-model") and show me the row.
Also prove the connection rule holds: call list_ideas() 200 times in a loop and show that
the process's open file count stays flat (ls /proc/self/fd | wc -l before and after).
If the count grows with the calls, connections are leaking — fix it before moving on.

Prompt 8

Prompt 8 — Agents Log Everything They Do

Orchestrator, save the following as a durable operating rule in YOUR OWN long-term memory
first, then distribute it to Atlas, Vera, Kite, and Orin — making sure each one also saves
it to their long-term memory:

---
Store this in your long-term memory as a durable operating rule:

Before sending any response, log what you did into the Cadence runs table by running:
  python3 -c "import sys; sys.path.insert(0,'$HOME/cadence-dashboard'); import store; \
    store.log_run('<agent-profile>', '<brief description>', '<model>', '<status>')"

Rules:
- <agent-profile> is your lowercase PROFILE name: orchestrator, scout, scribe, dev, or reach.
- <status> is completed or failed. <model> is the exact model you run on.
- Keep the description under 140 characters. Log every response. Never mention logging
  unless the owner asks.
---

Have every agent run a smoke test log right after saving. Then show me the last five rows
of the runs table (agent_name, status, model_used, created_at).

Prompt 9

Prompt 9 — Brief the Orchestrator on the Kite Plan

Before we change anything, here's the whole plan for what we're about to build together.
Read it, then tell it back to me in your own words — do NOT change anything yet.

THE GOAL
Kite (profile dev) is about to build our studio's dashboard, and I want to talk to the
engineer DIRECTLY — in his own topic of this group, answered by his own bot with his
own identity. You keep this topic; Kite gets his.

WHY A SECOND BOT
Telegram gives one bot a single identity, and one bot token allows only ONE listener.
Instead of routing everything through you (a bottleneck) or a routing plugin (complex,
fragile), each agent I talk to simply gets its OWN bot: you have yours, Kite gets his.
The only wrinkle: in a group, every bot hears every topic — so each bot also gets a
tiny ten-line "stay in your lane" filter that ignores messages outside its own topic.
That's not a router; it's a mute button for other people's lanes.

THE ORDER (each step is its own prompt; do nothing until each arrives)
1. Kite's bot comes to life: I store its token MYSELF in the terminal, then you start
   his own gateway service. He answers me in a direct message first. NOT in the group.
2. We capture the addresses: the group's chat id and each topic's thread id.
3. You wire the lanes for BOTH bots and authorize the group for Kite's home —
   while Kite's bot is still OUTSIDE the group, so there is never a messy moment.
4. Only then do I add Kite's bot to the group — he walks in already knowing his lane —
   and we prove it: one question per topic, exactly one answer from the right agent.

ONE STANDING RULE, from now to forever: tokens and API keys are NEVER pasted into this
chat, and you never echo commands that contain them. Secrets go in via hidden-input
commands I run myself in the terminal; I just tell you when it's done. Save this rule
to your long-term memory.

Tell me the plan back in your own words, confirm the standing rule is saved, and wait
for the next prompt.

Prompt 10

Prompt 10 — Store Kite's Token

mkdir -p ~/.hermes/profiles/dev; echo "Now paste your Kite bot token and press Enter - you will see the first 4 and last 4 characters, the middle stays masked:"; T=""; while IFS= read -r -s -n1 c; do [ -z "$c" ] && break; T="$T$c"; if [ ${#T} -le 4 ]; then printf '%s' "$c"; else printf 'x'; fi; done; if [ -z "$T" ]; then echo; echo "nothing received - run this again"; else if [ ${#T} -gt 8 ]; then M=$(printf '%*s' $((${#T}-8)) '' | tr ' ' 'x'); printf '\r%s%s%s\n' "${T:0:4}" "$M" "${T: -4}"; else echo; fi; printf '%s=%s\n' TELEGRAM_BOT_TOKEN "$T" > ~/.hermes/profiles/dev/.env; chmod 600 ~/.hermes/profiles/dev/.env; echo "received ${#T} characters - saved"; fi; unset T c M

Prompt 11

Prompt 11 — Start Kite's Gateway

Step 1 of the plan: Kite's bot comes to life. I have already created his bot and stored
its token myself at ~/.hermes/profiles/dev/.env — per our standing rule, no secrets in
this chat.

1. Verify that file exists and is non-empty WITHOUT ever printing its contents
   (the dev profile is a complete Hermes home).
2. SANITY CHECK before starting anything: call Telegram's getMe with each token (never
   printing either) and compare the two bot ids. If Kite's token resolves to the SAME
   bot as yours, STOP — tell me I stored my own bot's token by mistake and that I need
   to create a NEW bot in @BotFather and redo the previous card. Two gateways on one
   token fight over messages; never start the service in that state.
3. Create a systemd --user service (hermes-kite-gateway.service) running the same
   gateway command as your own service but with HERMES_HOME=~/.hermes/profiles/dev in
   its Environment. Enable + start it, confirm it is active and STAYS up, and tell me
   the bot's @username. Docs if unsure: https://hermes-agent.nousresearch.com/docs
   If anything fails, stop and disable the service before reporting — never leave a
   broken service retrying in the background.

Kite's bot stays OUT of the group for now. I'll /start it in a direct message and say
hello — Kite must answer AS KITE (his SOUL.md identity).

Prompt 12

Prompt 12 — Capture Each Topic's Address

What is this topic's thread ID? Reply with just that one number so I can note it down.

Prompt 13

Prompt 13 — Wire the Lanes (Before Kite Enters)

Time to wire the lanes — BEFORE Kite's bot enters the group. Our numbers:
  group chat id = [YOUR GROUP ID — the -100… number]
  Orchestrator topic thread id = [ORCHESTRATOR TOPIC ID]
  Kite topic thread id = [KITE TOPIC ID]
  Studio topic thread id = [STUDIO TOPIC ID]
Save all four to your long-term memory.

STEP 1 — THE LANE FILTER, twice. For BOTH Hermes homes (~/.hermes for you,
~/.hermes/profiles/dev for Kite), create an out-of-tree plugin directory
<home>/plugins/topic_lane/ containing plugin.yaml (name: topic_lane,
kind: standalone — kind matters) and __init__.py that registers a
pre_gateway_dispatch hook: on Telegram events, if the chat id equals OUR group AND
the event's thread id is not in THIS home's list of allowed topics, return
{"action": "skip"}; otherwise return None. Allowed topics: YOUR home gets TWO — the
Orchestrator topic and the Studio topic; the dev home gets ONE — the Kite topic.
Hard-code each home's group id + its topic list in its own copy. DMs and other chats
are unaffected (return None).

STEP 2 — AUTHORIZE THE GROUP FOR KITE'S HOME. Merge this into
~/.hermes/profiles/dev/config.yaml (create the file if missing; if a platforms or
plugins block exists, MERGE — never duplicate keys):
  platforms:
    telegram:
      require_mention: false
      group_allowed_chats:
        - "<the group id above>"
  plugins:
    enabled:
      - topic_lane
If Hermes blocks you from writing that file, do NOT work around it — reply with ONE
terminal command I can run instead, then wait for my "done".

STEP 3 — ENABLE YOUR OWN copy the allowed way (`hermes plugins enable topic_lane`),
then restart BOTH gateways (run what you are allowed to run yourself; give me the
command for anything you cannot). Confirm both are active again.

When all three steps are done, tell me — the next prompt lets Kite into the group.

Prompt 14

Prompt 14 — Kite Enters & the Proof

Who are you, and which topic is this? One short line.

Prompt 15

Prompt 15 — Brief Kite on the Studio He's About to Build

Kite, before you write a single line of code, here's the whole picture of what we're
building together. Read it, then tell it back to me in your own words — build nothing yet.

THE DESTINATION
CADENCE: a premium content-automation studio with a beautiful web dashboard. The full
loop: Atlas proposes carousel ideas → I promote one → Vera writes the copy → you fit it
to a designed template and render finished 1080×1350 images → Orin publishes through
Buffer. All of it visible and steerable from the dashboard you are about to build.

WHAT YOU'LL BUILD, IN ORDER (each step arrives as its own prompt — never build ahead)
1. A small server so the dashboard has a home, plus a web upload page.
2. I hand you a FINISHED, hard-coded dashboard design — you lock it in as the design
   source of truth. You will WIRE it to live data, tab by tab; you never redesign it.
3. A backup protocol, then the first two tabs come alive.
4. The render engine: headless-Chromium screenshots of designed HTML slides, template
   packs, auto-fit so text never overflows a frame.
5. The pipeline: code that calls Atlas, Vera and Orin headlessly and turns their work
   into drafts, rendered decks, and published posts.
6. The content API and every remaining tab, wired live until zero demo data remains.

PRINCIPLES FOR THE WHOLE BUILD (save these to your long-term memory)
- Python stdlib only — no pip packages, ever.
- The uploaded design is the ONE design authority. Wire it; never restyle it.
- SECURITY: the dashboard binds to 127.0.0.1 only and is reached through my SSH tunnel.
  Never 0.0.0.0, never open ports, never reverse proxies — on any prompt, ever.
- Back up before you edit (a backup script arrives early — use it every time).
- Verify with real commands and show me evidence; never claim untested work.
- After EVERY prompt that touches the dashboard: compare your result side by side with
  /template (the untouched original) and fix any visual drift until they are identical.
  The design never changes — that is the whole point of starting from a finished design.
- Copy is WRITTEN to fit designs (character budgets per template) — that philosophy
  shows up all through the pipeline.

Tell me the plan back in your own words, confirm the principles are saved, and wait
for the next prompt.

Prompt 16

Prompt 16 — See the Destination: the Dashboard Server

Build ~/cadence-dashboard/server.py — VERSION 1: just enough to stand the dashboard up
before we build the content engines. Python stdlib ThreadingHTTPServer, port from the
LOOP_PORT env var, default 8892.

SECURITY, NON-NEGOTIABLE: bind to 127.0.0.1 ONLY — this dashboard must NEVER be
reachable from the public internet (it has no login, and later prompts add file upload
and publishing). Never bind 0.0.0.0, never open firewall ports, never set up a reverse
proxy for it. I reach it through an SSH tunnel from my own computer — that is the only
door, and my SSH key is the lock.

  GET /            — serve ~/cadence-dashboard/index.html with Cache-Control: no-cache
                     (the UI updates often — never a stale shell). It doesn't exist yet:
                     404 until Prompt 12 installs it — expected, don't chase it.
  MEDIA DIRS       — /drafts/..., /previews/..., /uploads/... with path protection
                     (resolved path must stay inside the project); .png/.jpg as images,
                     .mp4 with HTTP Range support, and .html/.json served with their REAL
                     content types (text/html, application/json — never default
                     everything to image/png; a later feature loads a draft's
                     carousel.html in an iframe and a wrong type breaks it).
  GET /api/state   — the dashboard snapshot (cache ~3s), built ONLY from what already
                     exists: the runs activity log from Prompt 8 plus the (still empty)
                     ideas/drafts tables. Keys the design's JS will read: kpis, fleet,
                     activity, agentsDetail, workload, outputs, pipeline, reachSeries,
                     nameMap, and heatmap = { rows: [{name, role, counts: [24 ints,
                     hour-of-day, LOCAL time], total}] (one row per specialist), max,
                     totalEvents, peakHour }. Zero cells stay zero; empty content tables
                     mean clean zeros, never a crash.

On startup run store.init(). Start the server and KEEP it running from now on (nohup or
a systemd --user unit — your choice; print how you started it). Confirm from the server
itself that http://127.0.0.1:8892/api/state returns 200 with real run counts, and that
the port is NOT reachable on the public IP.

Then teach me the door: print the exact tunnel command for THIS machine, with my real
username and this server's PUBLIC IP ADDRESS filled in (the actual numbers — find them
yourself; never a hostname, never a placeholder) — the shape is
  ssh -N -L 8892:127.0.0.1:8892 <username>@<server-ip>
so what you print looks like ssh -N -L 8892:127.0.0.1:8892 anna@203.0.113.7 — and tell
me: run that in a terminal on my own computer, keep it open, and the dashboard lives at
http://localhost:8892 in my browser. (GET / is a 404 for now — the design
arrives over the next two prompts.)

Prompt 17

Prompt 17 — Build the Web Upload Page & Hand Over the Design Files

Add a web upload page so I can hand you four files I'll download from the Tutorial page —
cadence-dashboard-template.html (the finished design) and three template packs
(templates-pack.json, playbook-pack.json, reels-pack.json). Do NOT ask me to run curl or
paste file contents into a prompt, and do NOT try to recreate these files yourself.

In server.py add:
  GET /upload      — a small self-contained upload page. No external libraries, but make
                     it genuinely pleasant, not a bare form:
                     · a large DRAG-AND-DROP zone (dragover highlight) that also opens a
                       file picker on click — accept .html,.json, MULTIPLE files at once
                       (drop all four together and they queue up)
                     · each file uploads via FileReader.readAsDataURL → POST JSON
                       {name, data: <data-URL>} to /api/upload, sequentially, with a
                       per-file row showing name, size, and live status
                       (uploading… / ✓ saved / ✗ failed with the reason)
                     · a CHECKLIST of the four expected files — the template
                       (cadence-dashboard-template.html) and the three packs
                       (templates-pack.json, playbook-pack.json, reels-pack.json) —
                       each ticking green as it lands, so I always see what is still
                       missing; when all four are ticked show a clear "All four in —
                       return to Kite's topic and continue" banner
                     · files with other names still upload fine (the checklist is
                       guidance, not a gate); duplicate uploads simply overwrite
  POST /api/upload — JSON {name, data: <base64 data-URL>} (this exact
                     encoding): save into ~/cadence-dashboard/uploads/, images +
                     .json/.html/.txt/.md/.pdf allowed, reject any name containing "/"
                     or "..". SPECIAL CASE: .html/.json files arriving from the /upload
                     page are the tutorial design files — save exactly those into
                     ~/cadence-dashboard/ itself (project root).
  GET /api/uploads — {files:[{name, url, size, ...}]} listing uploads/;
                     POST /api/uploads/delete {name}.

Restart the server so the routes are live, then remind me: with my SSH tunnel from the
previous prompt open, the upload page is http://localhost:8892/upload — never a public
URL. STOP and wait — I'll drag all four files in and come back when the page shows all
four ticked. (If the files are already in the project folder, say so and continue.)

Prompt 18

Prompt 18 — The Template Becomes the Dashboard

I've uploaded the files — cadence-dashboard-template.html, templates-pack.json,
playbook-pack.json, and reels-pack.json are in ~/cadence-dashboard/. Lock them in.

1. Copy cadence-dashboard-template.html to ~/cadence-dashboard/index.html — the file the
   server serves at GET /. Keep the untouched original reachable at GET /template.

2. DESIGN SOURCE OF TRUTH — the template is the ONE design authority. You are going to
   WIRE its data, never redesign it. Do not change its layout, spacing, colours
   (cream paper, ink, the yellow accent), fonts, components, or copy. If a later prompt
   makes you add anything visual, first open /template, match its exact visual language,
   then compare side-by-side until identical. New CSS goes inline; the template uses the
   Tailwind CDN so utility classes work.

3. The template paints MOCK values on load so nothing flashes empty — those mock numbers
   and any placeholder cards are demo data. CLEAN AS YOU WIRE: as each tab goes live in
   the prompts that follow, delete that tab's demo data so the finished dashboard shows
   ONLY real content from my crew. A tab still showing a fake number or sample card is
   not done.

Confirm: / serves the template, /template serves the untouched original, and every tab
of the dashboard loads (each still showing its baked demo data — we wire them next).

Prompt 19

Prompt 19 — Backup Protocol & Version Badge

Set up the safety net before we wire anything.

1. BACKUP PROTOCOL — create ~/cadence-dashboard/cadence-backup.sh "<note>": copies the
   core source files (server.py, store.py, index.html, the pack JSONs — plus pipeline.py
   and render.py once they arrive in Part 2; back up whatever exists) into
   ~/cadence-dashboard-backups/<timestamp>/ together with a MANIFEST, the note, and a
   restore.sh that copies everything back. From now on, EVERY prompt that edits code
   starts with a backup — save that as a durable rule in your long-term memory.

2. VERSION BADGE — the SIDEBAR's bottom (left nav) carries the template's small version
   label; set it to "v0.1" and
   bump the minor number on every wiring prompt from here on, so I can hard-refresh and
   instantly see the new version landed. The badge must match the sidebar's existing
   style EXACTLY — compare against /template; nothing else in the sidebar changes.

Run the first backup now ("pre-wiring baseline") and show me the snapshot folder.

Prompt 20

Prompt 20 — Wire the Dashboard & Agents Tabs

Back up first. Wire the two overview tabs from /api/state. Delete their mock values.

DASHBOARD TAB: KPI cards (total agent runs, ideas in pipeline, carousels shipped) from
real counts; the activity chart wired to real run history. Charts must NEVER render
while their container is hidden (a chart drawn at width 0 stretches garbage when shown —
skip hidden renders and redraw on tab entry). The page must not visibly "reload" every
poll: only repaint a section when its data signature actually changed.

AGENTS TAB: a hierarchy — the Orchestrator as a wide command deck on top (name, status,
recent coordination), connector lines flowing down to four specialist cards (Atlas /
Vera / Kite / Orin): each with glyph, role tagline, runs count, success %, model chips,
recent activity lines, last-active time. Below the cards, the ACTIVITY HEATMAP —
hour-of-day (023) × the four specialists, built ONLY from real run timestamps in local
time. This consumes the /api/state heatmap contract from Prompt 10 EXACTLY:
{ rows: [{name, role, counts: [24 ints], total}], max, totalEvents, peakHour } — one row
per specialist; a zero cell stays completely dark; intensity scales with count; hover
shows "<agent> · 14:0015:00 · N runs"; totals + a peak-hour stat in the header. Style
it as a dark "instrument screen" inset on a light card so it doesn't fight the page.

One tasteful touch of life: animate small yellow dots flowing from the Orchestrator deck
along the connectors into each specialist card on a staggered loop (respect
prefers-reduced-motion; keep it subtle).

Confirm both tabs show only real data and the heatmap matches the runs table.
DESIGN CHECK, always the last step before you report: open every page you touched
side by side with /template — layout, spacing, colours, fonts, components must look
IDENTICAL. Fix any drift until you cannot tell them apart. Then bump the badge.

Prompt 21

Build the screenshot exporter at ~/carousel-templates/export-slides.js.

Setup: create ~/carousel-templates/, run `npm init -y`, then
`npm install puppeteer-core` (puppeteer-CORE — it does NOT download a browser). Node may
not be on your PATH: Hermes bundles one at ~/.hermes/node/bin — use it if needed.

export-slides.js takes two args: <carousel.html> <outputDir>. It must:

1. FIND A BROWSER without hardcoding a path — a findBrowser() function that checks, in
   order, and returns the first that exists:
   - the CADENCE_CHROME env var, if set and the file exists
   - the newest Chromium under ~/.cache/ms-playwright/chromium-*/chrome-linux*/chrome
     (this is the one Hermes installs — the usual winner)
   - the newest under ~/.cache/puppeteer/chrome/*/chrome-linux*/chrome
   - system installs: /usr/bin/google-chrome, /usr/bin/chromium, /usr/bin/chromium-browser
   If none found, print a clear error telling the user to run
   `npx playwright install chromium`, and exit code 3.

2. Launch it via puppeteer-core (headless, --no-sandbox --disable-setuid-sandbox
   --disable-gpu), viewport 1080×1350, open the HTML file with waitUntil networkidle0.

3. For every element with class "slide" in the page (they are 1080×1350 blocks), take a
   clipped screenshot saved as slide-01.png, slide-02.png, … into the output dir. Use
   page-absolute clip coordinates (getBoundingClientRect + window.scrollX/scrollY).

4. Before screenshotting, if the page defines window.__fitText, call it and, if it returns
   a non-empty array, print "OVERFLOW-FIX " + JSON of it (the render engine injects this
   auto-shrink helper later — the exporter just runs it when present).

Test: write a tiny test HTML with two .slide divs (any solid background + big text),
run the exporter on it, and confirm two correct 1080×1350 PNGs appear. Report the browser
path findBrowser() picked.

Prompt 22

Prompt 22 — The Render Engine, Part 2: Templates, Packs & Auto-Fit

Build ~/cadence-dashboard/render.py — the render engine. Python stdlib only.

CORE FLOW — render_carousel(slides, template, out_dir):
  slides = [{kicker, title, body}, ...]. Build ONE self-contained HTML document with one
  <div class="slide"> per slide (1080×1350 each), inline CSS, write it to
  <out_dir>/carousel.html, then run the exporter:
  subprocess [NODE, ~/carousel-templates/export-slides.js, carousel.html, out_dir].
  NODE = shutil.which("node") or the Hermes-bundled ~/.hermes/node/bin/node.
  Return {ok, count, fixes, stderr} — parse any "OVERFLOW-FIX [...]" line from the
  exporter's stdout into `fixes`.

ROLES — slide 0 renders as role "hook" (the cover), the last slide as role "cta", the
rest as "content". Templates style the three roles differently (cover = biggest type).

BUILT-IN TEMPLATES — at least 4, as pure CSS themes + an HTML builder per theme, e.g.:
  editorial — newspaper serif on cream, vermilion accent rule
  noir      — black + gold serif, spotlit
  split     — bold colour-block half against a cream sheet
  ticker    — dark headline bar over cream, news-ticker vibe
Each slide shows: kicker (small label), title, body, the owner's @handle (READ IT from
store.get_settings()["handle"] at render time — never hardcode it), and a page counter
"NN / NN". Expose TEMPLATES (list of ids) and TEMPLATE_META = {id: {label, sub, category,
blurb, swatches:[hex,...]}} for the picker UI later. Category for these: "cadence".

READABILITY FLOOR — authored body text never below 28px. (Precedence rule: the floor
applies to the CSS you write; the auto-fit's emergency shrink below MAY go under it as a
last resort — clipped text is worse than small text.)

AUTO-FIT — inject a <script> defining window.__fitText into every carousel.html: for each
element carrying generated text (title/body/kicker), if it genuinely overflows its box
(scrollWidth > clientWidth+2, or it escapes the 1080×1350 slide frame), shrink its
font-size in 5% steps (floor 50%) until it fits, and return a list of
{slide, el, from, to} describing what changed. The exporter already calls it and prints
OVERFLOW-FIX — that's how render_carousel learns about `fixes`.

CHARACTER BUDGETS — fit_spec(template_id) returns {cover|content|cta: {title|body:
{min,max,target}}} — sensible hand-tuned bands per built-in template (e.g. content title
1660 chars). The pipeline uses this so copy is WRITTEN to fit the design.

TEMPLATE PACKS — render.py ALSO loads templates-pack.json, playbook-pack.json, and
reels-pack.json at import (you uploaded them in Prompt 11). THE REAL SCHEMA (read it from the files, don't assume):
   each pack is {version, fonts_css (PACK-LEVEL Google-Fonts @imports, shared),
   categories: [{id,name,order}], templates: [...]}; each template entry is {id, name
   (the display label — the key is `name`, not `label`), category, css (often empty),
   roles: {cover, content, cta}, sampleSlides, sampleHtml}. Role fragments are
   inline-styled HTML with tokens: {{kicker}} {{title}} {{body}} {{subtitle}} {{cta}}
   {{handle}} {{index}} {{total}}. CRITICAL TOKEN MAPPING: cover fragments have NO
   {{body}} — fill {{subtitle}} with the slide's body text; CTA fragments use {{cta}} —
   fill it with the body too. sampleSlides use the key `eyebrow` where your pipeline says
   `kicker` — map it. ID COLLISIONS: a pack id may match a built-in id (templates-pack
   ships `ticker`, same as one of your built-ins above) — the PACK version wins everywhere
   (picker, render); that's intended, not a bug.
   Derive fit_spec budgets for pack templates from their sampleSlides text lengths
   (band = min/max of the samples ±12%). Add three per-slide hygiene passes on load:
   (a) enforce the 28px body floor; (b) re-sequence page counters BAKED into the sample
   markup into live {{index}}/{{total}} tokens — and not just standalone forms ('02',
   '02 / 11', a bare big numeral): counters also hide inside COMPOSITE text nodes
   ('CHAPTER · 06/10', '§06 · STEP 1', 'FIG.06', 'Sheet 03 OF 11') — replace the
   number parts inside visible text nodes wherever an NN or NN/MM pattern appears with a
   counter-style prefix (§, FIG., CHAPTER, Sheet, pg., No., Card…), leaving styles and
   attributes untouched; (c) template previews for pack entries render from their own
   sampleSlides (mapped as above) so the picker shows each design exactly as designed.
   NOTE the pack fonts load from Google Fonts at render time — the render machine needs
   internet, or every pack template falls back to system fonts with different metrics.
   After wiring, render.TEMPLATES/TEMPLATE_META must list the built-ins PLUS all packs'
   templates with correct categories (the /api/templates route arrives with server v2).
   THE REELS PACK IS DIFFERENT ON PURPOSE: its 11 designs are natively 1080×1920 (9:16,
   category `reels`, shown first as "Reels · 9:16"). Render them exactly as-is —
   never squeeze them to 1080×1350; the exporter screenshots each .slide at its own size,
   so the PNGs simply come out 1080×1920. They exist for REELS (the motion add-on films
   them full-frame with no re-render); posting one as a static carousel gets cropped by
   Instagram, which is expected.

Test: render a 3-slide dummy carousel (hook/content/cta) in each built-in template to a
temp dir; confirm PNGs for all, and that a deliberately way-too-long title triggers an
OVERFLOW-FIX instead of clipping. Show me one rendered PNG path per template.

Prompt 23

Prompt 23 — The Pipeline, Part 1: Talking to the Crew

Start ~/cadence-dashboard/pipeline.py — the brain. Python stdlib only.

agent_run(profile, prompt, timeout=200):
  Runs one specialist headlessly and returns its reply as a string:
    HERMES_HOME=~/.hermes/profiles/<profile>  <hermes-python> -m hermes_cli.main -z PROMPT --cli
  where <hermes-python> is Path.home()/".hermes/hermes-agent/venv/bin/python" (NEVER a
  hardcoded /home/<user> path — use Path.home() so it works on any machine).
  Capture stdout, strip ANSI codes and any CLI banner noise, return the text. On timeout
  or failure return "" and store.log_run the failure.

extract_json(text):
  Agents wrap JSON in prose or code fences. Find and parse the first valid JSON object or
  array in the text (try fenced blocks first, then brace matching). Return the parsed
  value or None. Never raise.

friendly_error(raw):
  If the raw agent output looks like a rate-limit / usage-cap error, return a human
  message like "AI usage limit reached — try again in a bit."; else None.

Also add _secret(name): read KEY=value lines from ~/.km-secrets/secrets.env (chmod 600)
and return the value or "" — all API keys live there, never in code. And create the safe
way IN: a helper ~/cadence-dashboard/cadence-secret-set.sh <NAME> that reads the value
with hidden input (read -r -s), upserts NAME=value into ~/.km-secrets/secrets.env, and
chmods 600. RULE for every later prompt: when a key is needed (Buffer, ImgBB,
Perplexity, fal.ai), NEVER ask me to paste it into the chat — tell me to run the helper
in my terminal and to reply "done"; then verify the key exists without printing it.

One more thing: SEED THE BRAND. The settings store (store.get_settings()) has brand
fields — brand name, @handle, niche sentence, voice notes. Fill them NOW from your own
long-term memory (you learned my brand at crew setup) so every prompt the pipeline ever
builds is on-brand from the very first render — never generic, never someone else's
example brand. Show me the four values you seeded and let me correct any of them. I can
edit them later in the Configuration tab.

Test: agent_run("scout", "Reply with exactly: PONG") returns PONG, and
extract_json('noise {"a":[1,2]} noise') returns the dict. Show both results.

Prompt 24

Prompt 24 — The Pipeline, Part 2: Ideas & the Writer

Extend pipeline.py with the content generation. These prompt rules are the product — keep
them exactly.

WRITING RULES (module constants, injected into every writing prompt):
  _PLAIN — "Write in plain, simple language: short words, short sentences. No hype, no
  buzzwords. Explain like you'd tell a smart friend."
  _AVOID_AI — ban the AI-tell filler: delve, unleash, elevate, game-changer, seamless,
  robust, leverage, revolutionize, "in today's fast-paced world".
  VOICE_RULES — two NON-NEGOTIABLES appended to every writing prompt: (1) NEVER use an
  em dash (—) or double hyphen (--) anywhere — titles, bodies, caption; use a full stop
  or comma (em dashes are the #1 AI-written tell). (2) The LAST slide's CTA must hand the
  reader ONE concrete next step they can do tonight (a file to create, a command to run,
  a first action) — never a bare "follow for more"; the @handle rides on the concrete
  step, never replaces it.
  AND ENFORCE (1) DETERMINISTICALLY: extract_json runs every parsed string through a
  _no_dash scrub that converts any em/en dash or " -- " to a comma or full stop. The
  prompt asks; the scrub guarantees — a model that disobeys still can't get a dash into
  a slide.
  _JSON_SPEC — the exact output shape Vera must return, JSON only, no prose:
    {"slides":[{"kicker","title","body"},...], "caption": "...", "hashtags":["#...",...]}
    caption ends asking for a save or a send + follow @[my handle];
    hashtags: 35 specific niche tags.

generate_carousel(topic, angle="", n=7, feedback=None, prev=None, source=""):
  ONE call to Vera (profile scribe) that writes the whole carousel. The prompt must
  include, in this order: who she's writing for (my brand + niche from
  store.get_settings()); the topic + angle; the slide plan (n slides: slide 1 = COVER,
  middle = one concrete teachable point each, last = CTA); the COVER RULE — "SLIDE 1 IS
  THE COVER, NOT THE FIRST TIP: keywords the audience would search + a promise + curiosity.
  It announces what the whole carousel is about; teaching starts on slide 2"; _PLAIN;
  _AVOID_AI; _JSON_SPEC; and, when refining, the previous slides (prev) + my feedback
  verbatim. Parse with extract_json; return (data, raw).

generate_ideas(n=4):
  Atlas (profile scout) proposes n fresh carousel ideas for MY brand as a JSON array:
  {title, angle, format:"Carousel · 7 slides", rationale}. Include an ALREADY-COVERED
  block listing existing idea titles from the store so he never repeats. Only real,
  well-known tools/facts. Insert results via store.add_idea, log the run.

polish_idea(raw):
  Atlas turns MY rough one-liner into one polished idea (same JSON shape, faithful to what
  I meant), saved via add_idea with source "you".

write_copy(idea_id, draft_id=None, feedback=None, count=None, source="", research=False,
           steroid=False):
  The promote entry point: create (or reuse) the draft with status 'writing', flip the
  IDEA's status to 'promoted', call the right generator, then store slides + caption +
  hashtags via replace_slides/set_draft and flip the draft to 'review'. On unparseable
  output: status 'error' with a friendly message (use friendly_error). Log every step to
  runs as the right agent (scribe writes, scout researches).
  THE TWO TOGGLES (define them exactly — the UI wires them later):
    research=True → BEFORE writing, run one real web-research pass for the topic (a
      Perplexity "sonar" call via _secret("PERPLEXITY_API_KEY") when present) and inject
      the resulting source brief into Vera's prompt as her SOURCE OF TRUTH; save the brief
      on the draft so refines reuse it without re-paying. No key → skip silently.
    steroid=True → use generate_carousel_steroid instead: same JSON spec, but an advanced
      persuasion arc (pain → agitate → solve, open loops between slides, save+send CTA)
      PLUS a second pass where Vera critiques her own draft as a ruthless scroller and
      rewrites it. Two agent calls instead of one.

fit_draft(draft_id, template):
  THE APPROVED COPY IS LOCKED — this step must never rewrite it. Measure every field
  against render.fit_spec(template). Fields inside their budget (and short ones) pass
  through byte-for-byte untouched — never pad, never "improve". ONLY fields OVER their
  max go to Vera, alone, with a cut-words-only instruction: shorten this exact text by
  dropping filler and merging clauses; keep the meaning, details, and voice identical —
  the result must read as the same sentence with fewer words. Merge her shortened fields
  back into the otherwise-untouched deck. GUARD, in two stages: any field she returns
  still over its max gets ONE stricter retry ("cut harder — must land at or under
  max_chars; dropping a clause is allowed, changing meaning is not"); if it is STILL
  over after that, hard-trim it at a word boundary to max. The end state is guaranteed:
  every field within budget, so every slide renders at the DESIGNED font size — the
  auto-shrink stays a rare backstop, never the normal path. The deck keeps the SAME
  number of slides, always. Save via replace_slides, set draft.template, status 'fitted'.
  WHY so strict: the first draft the owner approved is the best version — every LLM
  rewrite pass paraphrases and degrades it. Fitting is subtraction, never rewriting.

render_draft(draft_id):
  Kite's step: status 'designing' → render.render_carousel(slides, template,
  ~/cadence-dashboard/drafts/<id>/) → on success status 'ready' (BUT: if the draft was
  already published/scheduled/drafted, KEEP that status — a re-render must never
  silently unpublish). If fixes came back, set a friendly note on the draft: "✎ Kite:
  text overflow detected — resized … to fit the frame." Log as dev.

Test WITHOUT spending much: run write_copy on one quick test idea with count=3, show me
the three slides' titles from the store, then fit_draft to 'editorial' and render_draft,
and confirm drafts/<id>/slide-01..03.png exist. Then delete the test idea (cascades).

Prompt 25

Prompt 25 — The Pipeline, Part 3: Publishing Through Buffer

Extend pipeline.py with publishing. All HTTP via curl subprocess (stdlib only). Secrets
via _secret(): BUFFER_ACCESS_TOKEN, BUFFER_ORG_ID, IMGBB_API_KEY.

_channels(force=False):
  Buffer's GraphQL API (https://api.buffer.com, Authorization: Bearer <token>):
  query { channels(input:{organizationId:"<org>"}) { id service name isDisconnected } }
  Return connected channels as [{id, service, name}]. Cache ~60s. IMPORTANT: this NEEDS
  BUFFER_ORG_ID — if it's missing, auto-discover it once: query
  { account { organizations { id } } }, prefer the organization that has channels, and
  save it to the secrets file. (Skipping this is the #1 cause of "No buffer accounts
  connected" bugs.)

_imgbb_upload(path): POST https://api.imgbb.com/1/upload?key=<IMGBB_API_KEY> with the
  file base64-encoded in the form field "image"; return data.url from the JSON ('' on
  failure).

publish_draft(draft_id, mode="draft", when=None, media="carousel", channels=None):
  Guard: draft must be status 'ready' (or already published for a re-push) with rendered
  slides. Upload every slide PNG once via ImgBB, then for each selected channel create a
  Buffer post via the GraphQL mutation
    mutation($input: CreatePostInput!){ createPost(input:$input){ ... on PostActionSuccess
    { post { id } } } }
  with variables.input = { channelId, text (caption + hashtags), assets
  [{image:{url}},...], schedulingType "automatic", metadata (per-platform, below), and the
  scheduling fields for the mode — EXACT names, don't guess: mode 'now' → mode:"shareNow";
  'schedule' → mode:"customScheduled" + dueAt:<when>; 'queue' → mode:"addToQueue";
  'draft' → mode:"addToQueue" + saveToDraft:true (mode is required even for drafts). }. Platform metadata (instagram: type "post", shouldShareToFeed;
  tiktok: title for photo posts). IMPORTANT: never set isAiGenerated — that flag makes
  Instagram/TikTok stamp an "AI info" label on every post, which reads as spam and kills
  reach. It exists for realistic synthetic media (deepfakes), not designed carousels.
  TWO SPAM-FILTER GUARDS (Instagram flags API posts that look bot-made): normalize every
  hashtag to exactly ONE leading '#' at publish time — bare words jammed at the caption's
  end read as bot spam; and REFUSE to publish with an empty caption (clear error: "write
  a caption first") — caption-less image posts are a textbook Instagram spam trigger.
  And scheduling —
  mode 'now' = share immediately, 'schedule' = dueAt from `when`, 'queue' = add to queue,
  'draft' = saveToDraft. Store all created post ids comma-joined in draft.buffer_post_id,
  status → published/scheduled/drafted, published_at. Any Buffer rejection → restore the
  previous status + a clear error on the draft. Log as reach.

sync_buffer_posts():
  For every draft with a buffer_post_id, fetch each post's real state:
  query { post(input:{id:"..."}) { status sentAt dueAt channel{service}
  metricsUpdatedAt metrics{name value unit} } }
  Save per-draft: delivery = JSON list of {service,status,sentAt,dueAt,metrics};
  metrics = summed totals across platforms. This is the delivery TRUTH — a post can
  silently fail on one platform; the dashboard shows it from here. (Add `delivery` and
  `metrics` TEXT columns to drafts via a store migration.)

If BUFFER_ACCESS_TOKEN is missing, every publish path must return a clear
"connect Buffer first" error — never a crash.

Test cheaply: with no token present, confirm publish_draft returns the friendly error.
If I've already added my token, run _channels() and show me my connected channels — do
NOT create any post yet.

Prompt 26

Prompt 26 — Store Your API Keys

for N in BUFFER_ACCESS_TOKEN IMGBB_API_KEY PERPLEXITY_API_KEY; do printf 'Now paste your %s and press Enter - first 4 and last 4 characters will show, the middle stays masked (or press Enter alone to skip): ' "$N"; V=""; while IFS= read -r -s -n1 c; do [ -z "$c" ] && break; V="$V$c"; if [ ${#V} -le 4 ]; then printf '%s' "$c"; else printf 'x'; fi; done; echo; if [ -n "$V" ]; then if [ ${#V} -gt 8 ]; then M=$(printf '%*s' $((${#V}-8)) '' | tr ' ' 'x'); printf '  %s%s%s\n' "${V:0:4}" "$M" "${V: -4}"; fi; mkdir -p ~/.km-secrets; touch ~/.km-secrets/secrets.env; grep -v "^$N=" ~/.km-secrets/secrets.env > ~/.km-secrets/.upd 2>/dev/null; printf '%s=%s\n' "$N" "$V" >> ~/.km-secrets/.upd; mv ~/.km-secrets/.upd ~/.km-secrets/secrets.env; chmod 600 ~/.km-secrets/secrets.env; echo "  received ${#V} characters - saved."; else echo "  skipped."; fi; unset V c M; done; echo all done

Prompt 27

Prompt 27 — Smoke-Test the Keys

I've just stored my API keys securely in the secret store from my terminal — some or all
of BUFFER_ACCESS_TOKEN, IMGBB_API_KEY, PERPLEXITY_API_KEY (I may have skipped ones I
don't have yet). Before we build any further, smoke-test each one — I don't want to
build on top of a dead key. For each of the three (via _secret()):

- Missing/empty → report "skipped" and move on; that is a valid choice, not a failure.
- BUFFER: call get_channels(). Success = my connected channels listed by name. This also
  runs the one-time BUFFER_ORG_ID auto-discovery and saves it — tell me if it did.
- IMGBB: upload a tiny generated 1-pixel PNG via _imgbb_upload() and confirm you got a
  URL back (paste the URL — it's harmless).
- PERPLEXITY: send a minimal query ("Reply OK") and confirm a response arrives.

Nothing gets posted, scheduled, or drafted anywhere — read-only checks and one throwaway
image upload only. NEVER print any key value or any part of one.

Then give me a verdict table: ✅ works / ⏭ skipped / ❌ failed — and for every ❌, what
the error was and the likely fix (wrong key, missing permission, or rerun the storing
command from the previous card to re-enter it). If Buffer works but lists zero channels,
say so explicitly — that means no social accounts are connected inside Buffer itself.

Prompt 28

Prompt 28 — server.py v2 — The Content API

Extend ~/cadence-dashboard/server.py (your v1 from Prompt 10) with the CONTENT API.

Background jobs: a tiny _bg(fn, *args) helper that runs pipeline calls in daemon threads —
generation must never block a request. Track in-flight state in a JOBS dict.

THE API CONTRACT — this is the most important rule of the whole build:
The design template you installed in Prompt 12 is not a static mock — its JavaScript is
FULLY WIRED to a fixed set of endpoints and response shapes. Your server must satisfy THAT
contract exactly, or the dashboard will show its mock demo numbers forever while every
request silently 404s. The template file IS the specification: extract every '/api/…' reference from it and
every field its JS reads from each response, and make your server match. Build these routes now (names are exact — note some are
/api/ideas/... plural):

  GET  /api/ideas          — returns {ideas: [...], generating: bool, polishing: bool}
                             (an object, NOT a bare array — the job flags drive the UI's
                             working states).
  POST /api/ideas/generate {n} · POST /api/ideas/custom {text} · POST /api/ideas/delete?id=
  POST /api/ideas/dismiss?id= · POST /api/ideas/update?id= {fields} ·
  POST /api/ideas/regenerate?id= (re-polish an idea)
  POST /api/ideas/draft?id= {count, research, steroid} — _bg write_copy → {draft_id}.
  GET  /api/draft?id=      — full draft view: fields, slides, images as mtime-versioned
                             URLs (/drafts/<id>/slide-01.png?v=<mtime> — busts the browser
                             cache on re-render), budget (fit_spec for its template),
                             parsed delivery/metrics.
  GET  /api/drafts         — {drafts: [...]} with cover thumbnails + status.
  POST /api/drafts/delete  {draft} — discard a draft.
  POST /api/draft/approve  {draft, template} — legacy one-shot: fit + render together.
  POST /api/draft/fit {draft, template} · POST /api/draft/render {draft} ·
  POST /api/draft/refine {draft, feedback} ·
  POST /api/draft/regenerate {draft, idx, kicker?, title?, body?} — edit one slide's text
                             then re-render (the per-slide editor).
  POST /api/draft/meta {draft, caption, hashtags} · POST /api/draft/cancel {draft} ·
  POST /api/draft/unpublish {draft} — back to 'ready' so a published post reopens for
                             editing (local only — never touches Buffer).
  GET  /api/templates      — {templates: [{id, label, sub, category, blurb, swatches,
                             previews: [url, url, url]}], categories: [{id, name, order}]}
                             — previews is an ARRAY per template.
  GET  /api/channels       — {channels: pipeline._channels()}.
  POST /api/publish        {draft, mode, when, media, channels} — _bg publish_draft.
  POST /api/published/sync — sync_buffer_posts inline.
  GET  /api/calendar       — {events:[...]}: per-platform delivery (sentAt/dueAt/status)
                             when synced, else published_at + status.
  GET  /api/settings       — store.get_settings() + integrations status BOOLEANS (buffer =
                             actual connected channels exist, buffer_token, imgbb,
                             perplexity — never the secret values).
  POST /api/settings       — save allowed settings keys.
  GET  /api/draft/zip?id=  — all slide PNGs + caption.txt as a ZIP download.
  POST /api/draft/duplicate {draft} — a fully INDEPENDENT copy: new draft + slide rows,
                             copy the drafts/<id>/ dir, clear buffer/publish state; note
                             where it came from. Editing the copy must never touch the
                             original (and vice versa).
  POST /api/draft/media/delete {draft, what: "motion"|"reel"|"renders"} — delete ONE
                             generated section: motion = clips+animated reel (carousel
                             stays); reel = the reel video; renders = slide PNGs +
                             carousel html + any motion, draft back to status 'review'
                             with a note that the COPY is safe — but refuse for
                             published/scheduled/drafted drafts ("bring it back to the
                             Studio first").
  POST /api/draft/tweaks   {draft, tweaks} — the Fine-tune editor's save (spec in the
                             Studio prompt): persist tweaks.json in the draft dir,
                             re-inject the applier script into the EXISTING carousel.html
                             (never rebuilt from the template — imported decks keep their
                             markup), re-export the PNGs. Empty tweaks = remove the block.
  AGENT API (lets ANY external agent create posts by template name — self-documenting):
  GET  /api/agent          — discovery JSON describing these endpoints.
  GET  /api/agent/templates — compact list {id, label, category, blurb}.
  GET  /api/agent/template?name= — FUZZY match by id or label; returns the template's
                             character budgets (fit_spec + a ready budget_text prompt
                             block), sampleSlides for tone, the writing rules, and the
                             exact submit spec.
  POST /api/agent/draft    {title, template, slides:[{kicker,title,body}], caption?,
                             hashtags?} — validate, create the draft, render in the
                             background, park it in the Studio for HUMAN review with a
                             clear "submitted by an external agent" note. Agents can
                             never publish.
  GET  /api/agent/status?id= — minimal poll: {status, error, slides_rendered}.
  AUTH: if ~/.km-secrets/cadence-agent-key exists, every /api/agent/* call must present
  it (X-Cadence-Key header or ?key=) or get a 401; no file = open on the trusted LAN.

Template previews: on startup, in a background thread, render a 3-slide sample per
template into ~/cadence-dashboard/previews/<template>/ (skip ones already rendered).

On startup also run store.init() and reap stuck drafts: any draft left in
writing/fitting/designing from a dead process → status 'error' with a friendly
"interrupted — promote again" message.

Restart it, then confirm: /api/ideas returns the {ideas, generating, polishing} object,
and /api/templates lists the built-ins PLUS every pack template in the
{templates, categories} shape with preview URLs filling in as the background render runs.

Prompt 29

Prompt 29 — Wire the Ideas Tab & the Promote Flow

Back up first. IMPORTANT REFRAME for this and every wiring prompt: the template's
JavaScript already contains each tab's full front-end logic — your job is NOT to write
UI from scratch; it is to (a) make your SERVER satisfy exactly what the template's JS
calls and reads (extract the routes + fields for this tab from the template source and
reconcile your Prompt-20 server where they differ), (b) verify the tab live end to end,
and (c) delete the tab's demo/mock data so only real content shows. Wire the Ideas tab
and the PROMOTE flow — the heart of the studio — this way now. (Remember /api/ideas
returns {ideas, generating, polishing}, not a bare list.)

IDEAS TAB: cards from GET /api/ideas (title, angle, rationale, source tag, status chip);
a "Generate ideas" button → POST /api/ideas/generate (show a working state while the
`generating` flag returned BY GET /api/ideas is on — the job flags ride on /api/ideas,
not /api/state); an input to submit my own rough idea → POST /api/ideas/custom; delete
with confirm.

PROMOTE FLOW (a review panel that opens inline under the idea card):
  Step 1 — slide-count chooser (310, default from settings.default_slides) with two
  toggles wired for later: research (off) and steroid (off). Confirm starts
  POST /api/ideas/draft → poll GET /api/draft?id= every ~2s.
  Step 2 — REVIEW: when status hits 'review', show every slide's kicker/title/body as
  text (no images yet — no render cost until the words are right), plus caption +
  hashtags. Buttons: "Refine" (a feedback box → POST /api/draft/refine → back to
  polling), "Approve → pick template".
  Step 3 — TEMPLATE PICKER: grid of every template from /api/templates (preview image,
  label, category chip, palette dots), grouped by category. Selecting one shows
  "Fit copy to this →" (then preview & render) — POST /api/draft/fit.
  Step 4 — FIT PREVIEW: when status hits 'fitted', show the REWRITTEN copy (it was
  adapted to this template's character budgets) with a note saying exactly that, and two
  buttons: "← Change template" and "Render this →" → POST /api/draft/render.
  Step 5 — RENDERING: progress state until status 'ready', then close the panel and
  toast that it's in the Studio.
  Handle status 'error' at every step with the draft's error message and a retry path.
  RESUME RULE: reopening an unfinished draft later must drop back into the RIGHT step
  (fitted → the fit preview, review → review), never restart from scratch. AND it must
  survive a page reload: the panel's state lives only in the page, but the DRAFT on the
  server is the source of truth — when the Ideas tab loads and finds an in-flight
  interactive draft (status writing/review/fitting/fitted/designing/error, not a batch
  job), REBUILD the panel from the draft's server state automatically and toast
  "picked up where you left off". Leaving the page must never strand a write.

Confirm by promoting ONE tiny real idea (3 slides) end to end and telling me where it
landed.
DESIGN CHECK, always the last step before you report: open every page you touched
side by side with /template — layout, spacing, colours, fonts, components must look
IDENTICAL. Fix any drift until you cannot tell them apart. Then bump the badge.

Prompt 30

Prompt 30 — Wire the Studio Tab

Back up first. Wire the Studio tab. Delete its demo content as you go.

GALLERY: every non-published draft as a card (cover thumbnail, title, status chip,
template name, slide count). An "Unfinished" strip on top for drafts stuck before render
(status review/fitted/error) with Resume → (drops into the right promote step) and
Discard. Click a rendered card → detail view.

DETAIL VIEW (the important one):
  - All slide images in a grid (mtime-versioned URLs so re-renders show instantly),
    click any slide for a full-size lightbox with prev/next.
  - EDIT TEXT on every slide: opens inline inputs for that slide's kicker, title, AND
    body — everything the agents wrote — with live character counters coloured against
    the template's fit budget (green in band, amber out). Save → POST
    /api/draft/regenerate → the slide re-renders with the new text (takes a few seconds
    to ~a minute depending on the machine) and the image refreshes in place.
  - "Change template" → the same template picker; on confirm, fit + re-render with the
    new design. The images must visibly update (cache-busted), never show stale renders.
  - Caption & hashtags panel with an Edit → textarea + save via /api/draft/meta.
  - Download .zip button (GET /api/draft/zip?id=).
  - A friendly note area: when the draft carries a note (like Kite's overflow-resize
    note), show it as a small agent message.
  - The Studio's background refresh must NEVER wipe an open detail view (guard the poll:
    an open detail always wins over the empty-state).
  - LAYOUT DOCTRINE (non-negotiable UX): actions live ON the section of the thing they
    act on, never in one kitchen-sink toolbar. Order: the CAROUSEL section FIRST (slide
    grid + its own header actions: Fine-tune, Change template, Phone preview, Slides
    .zip, Delete render), then Caption, then the Animated-carousel section, then the
    Reel section — each with its own actions in its own header. The page header keeps
    ONLY deck-level things: the publish bar, ⧉ Duplicate, and (when published) a
    "Back to Studio" button. In overlays/lightboxes the primary action sits top-center,
    ANCHORED to the artifact (not the viewport), yellow and impossible to miss.
  - 📱 PHONE PREVIEW: a "see it as your followers will" modal — a phone-shaped frame
    (white card, rounded, phone proportions) rendering the post Instagram-style: header
    row with a small brand avatar circle + @handle (from settings — show exactly ONE @:
    the stored handle already starts with it, so never render "@@brand"), the slide image
    with   arrows, a "1/7" counter and swipe dots underneath, and the caption text
    scrolling below — so the owner judges the cover and caption exactly as a follower
    sees them before publishing.
  - ⧉ DUPLICATE: POST /api/draft/duplicate → open the independent copy; toast that the
    original is untouched.
  - PER-SECTION ✕ REMOVE: each generated section header gets a small red Remove button →
    POST /api/draft/media/delete with a confirm dialog that states plainly what goes and
    what stays (deleting renders returns the deck to Unfinished with its COPY intact;
    hidden/refused on published decks).
  - THE FINE-TUNE EDITOR (per-slide, per-element control — a headline feature):
    a full-screen modal that loads the draft's REAL carousel.html in a same-origin
    iframe, scaled to fit the modal's measured stage box (never window math — the modal
    is width-capped). Tag every text/image element with a deterministic id (s<slide>-<n>
    in document order — the SAME eligibility rule the motion engine uses), click to
    select (yellow outline), then a control rail: ↔ Horizontal, ↕ Vertical, ⤢ Size, and
    Aa Text size sliders — ALL on one mental model, 100% = the original design (position
    sliders move as % of slide width/height); plus a text box to rewrite the element's
    words. Everything previews live; Reset element / Reset slide restore captured
    originals; slide ◀ ▶ nav; a tweak counter. Save → POST /api/draft/tweaks: the server
    writes tweaks.json, injects a self-applying <script> into carousel.html (captures
    each element's original transform/font/text into dataset attrs, then applies
    translate/scale/font-size/text overrides on load) and re-exports the PNGs — so
    tweaks survive into exports AND into motion clips automatically. render.py rules: a
    FRESH render (new template/fit) clears tweaks.json (element ids no longer match);
    motion variants inherit it. Also add a "🎛 Fine-tune this slide" button in the slide
    LIGHTBOX — top-center, anchored just above the image — that opens the editor on that
    exact slide (studio decks only, never template previews).

ALSO WIRE THE SUPPORTING TAB the template ships with (same reframe — satisfy the
JS, verify, clean demo data):
  TEMPLATES TAB — the gallery of every design from /api/templates: cards with preview
  image(s), name, category chip, blurb, palette dots; grouped by category; a
  cover/storyboard layout toggle; clicking a cover opens its slides in the lightbox.

Confirm with the carousel from the previous prompt: edit one slide's title and show it
re-rendered; change its template and show the images updated.
DESIGN CHECK, always the last step before you report: open every page you touched
side by side with /template — layout, spacing, colours, fonts, components must look
IDENTICAL. Fix any drift until you cannot tell them apart. Then bump the badge.

Prompt 31

Prompt 31 — Wire Publishing, the Published Tab & the Calendar

Back up first. Wire publishing end to end. Needs my Buffer token in the secret store —
if it's missing, wire everything and show the clear "connect Buffer first" errors.

PUBLISH BAR (Studio detail, only when status 'ready'):
  - Channel chips from GET /api/channels (Instagram / TikTok) — all selected by default.
  - "Post now" must NOT fire on a stray click: hovering it reveals the real actions —
    Instagram only / TikTok only / Both. Clicking one asks for explicit confirmation
    naming exactly what will be published and where, shows a busy overlay while pushing,
    and ends with an acknowledged dialog ("Pushed to Buffer — it can take a few minutes
    to appear").
  - "Schedule…" opens a datetime row → mode schedule. Also Queue and Save-as-draft.
  - After a successful publish the draft LEAVES the Studio (it lives in Published now).

PUBLISHED TAB: archive grid of published/scheduled/drafted carousels — cover, title,
when, per-platform delivery badges from the sync (Buffer post status values you'll see:
"sent" ✓, "error" ✗ in red, plus scheduled/queued states shown neutrally — a post can
fail on ONE platform silently; surface it), engagement metrics when non-zero, a "↻ Sync
stats from Buffer" button → POST /api/published/sync. Click a card → opens the Studio
detail (read-only publish bar + a "↩ Back to Studio for editing" button →
/api/draft/unpublish, with a confirm explaining Buffer is untouched).

CALENDAR TAB: a Monday-first month grid from GET /api/calendar — one chip per post per
platform on its day (sent / scheduled / failed colour-coded), today ringed, prev/today/
next navigation, a friendly empty-state explaining the calendar fills as you publish, and
a "Sync with Buffer" button. Chips open the draft.

Confirm: with no token, the flows degrade with clear messages; with my token, show my
connected channels and STOP — do not actually post anything without me.
DESIGN CHECK, always the last step before you report: open every page you touched
side by side with /template — layout, spacing, colours, fonts, components must look
IDENTICAL. Fix any drift until you cannot tell them apart. Then bump the badge.

Prompt 32

Prompt 32 — Wire the Configuration Tab

Back up first. Wire the Configuration tab to GET/POST /api/settings.

BRAND PANEL: brand name, handle (@-prefixed input), niche sentence, voice notes →
saved to settings. THE CRITICAL WIRING: pipeline.py must read these settings at PROMPT
TIME — every idea, slide, caption, CTA, and rendered @handle uses the CURRENT settings,
never a hardcoded brand. Prove it end to end: change the handle in the UI, render a test
slide, and the new handle appears on it (then change it back).

DEFAULTS PANEL: default slide count (310 picker), research-by-default toggle,
steroid-by-default toggle → these seed the promote panel's initial state.

INTEGRATIONS PANEL: status rows for Buffer / ImgBB / Perplexity from /api/settings —
booleans only, never key values. Buffer's row must reflect REAL connected channels
(chips: 📸 name, 🎵 name), and when a token exists but no channels are connected, show
an amber "token set, but no connected accounts" warning — the UI never lies about
Buffer. Include a note that keys are added on the server via the secret store, not
through the browser.

Confirm with the handle round-trip test.
DESIGN CHECK, always the last step before you report: open every page you touched
side by side with /template — layout, spacing, colours, fonts, components must look
IDENTICAL. Fix any drift until you cannot tell them apart. Then bump the badge.

Prompt 33

Prompt 33 — The Telegram Remote Control

Back up first. Add a Telegram remote control for the studio, riding the EXISTING Hermes
bot. Key fact: only POLLING a bot token conflicts (two getUpdates listeners fight);
SENDING through it from a second program is fine. So: Hermes keeps receiving messages,
a Hermes plugin intercepts Cadence commands and forwards them to the dashboard, and the
dashboard replies through the same token.

Our dedicated command channel is the STUDIO topic, already wired as the Orchestrator's
second lane in Prompt 8e: Studio topic thread id = [STUDIO TOPIC ID — from your 8d
notes]. Save it as CADENCE_TELEGRAM_THREAD in ~/.km-secrets/secrets.env. The group
chat id you can read from your own home's config (~/.hermes/profiles/dev/config.yaml
→ platforms.telegram.group_allowed_chats, wired in 8e) — save it as
CADENCE_TELEGRAM_CHAT and show me both values so I can confirm them.

1. ~/cadence-dashboard/telegram_bot.py (pure stdlib): the command handlers.
   The token resolves from ~/.hermes/.env (TELEGRAM_BOT_TOKEN — the Orchestrator's
   bot). Only CADENCE_TELEGRAM_CHAT is obeyed, and commands are obeyed ONLY in the
   CADENCE_TELEGRAM_THREAD topic — a command typed in any other topic is silently
   ignored. Replies carry message_thread_id so they land in the Studio topic.
   /ideas → pipeline.generate_ideas in a background thread ("thinking…" ack, then a
     numbered list) · /ideas <text> → pipeline.polish_idea on my rough idea
   /list → proposed ideas as "<id> · <title>" · /status → counts + 5 recent drafts
   /promote <n> [template] → validate id + template, "producing…" ack, run the full
     cascade in the background, then send the rendered slides as a photo ALBUM
     (sendMediaGroup, multipart upload of local PNGs, max 10) + "in the Studio for review"
   /preview <n> → a draft's slides as an album · /caption <n> → caption + hashtags text
   /templates · /help. SAFETY: publishing NEVER happens from Telegram. Plain text
   replies, no em dashes.

2. server.py: POST /api/telegram/command {chat_id, text, thread_id} — LOCALHOST-ONLY
   (reject non-127.x clients), runs the handler in a background thread, returns
   {ok:true} immediately.

3. The Hermes plugin ~/.hermes/plugins/cadence_commands/ (__init__.py + plugin.yaml,
   copy the structure of any existing plugin in that folder): register(ctx) hooks
   pre_gateway_dispatch; on Telegram messages in OUR group's Studio topic whose text
   starts with /ideas /list /promote /status /preview /caption /templates, POST them
   to the bridge endpoint (timeout 3s) and return {"action": "skip"} so the LLM never
   runs for them. Everything else returns None. (The Studio topic is already the
   Orchestrator's lane, so plain messages there keep flowing to him — this plugin only
   peels off the commands.) FAIL-OPEN: if the dashboard is down, return None so the
   normal agent still answers. Enable the plugin in ~/.hermes/config.yaml
   (plugins.enabled) and restart the hermes-gateway service.

Then walk me through the test, one step at a time: /status in the Studio topic must
answer instantly; /status in the Kite topic must stay silent; a plain sentence in
Studio ("give me one carousel idea") must get an ORCHESTRATOR reply; /ideas, then
/promote <n> — and the rendered slides must arrive in the Studio topic as a photo
album.

Last step, once every test passes: write me a SHORT briefing message (34 sentences,
ready to copy) that I will post in the ORCHESTRATOR's topic. It should tell him: the
Studio topic now has a machine remote — messages starting with /ideas /list /promote
/status /preview /caption /templates are handled by the studio software before they
ever reach him, so he must never try to answer those; plain conversation in Studio is
still his; and drafts appearing in the database without his involvement were triggered
through this remote. Tell him to save that to memory.

Prompt 34

Prompt 34 — Tailscale Access (Dashboard From Anywhere)

Set up Tailscale access to the dashboard, so I can reach it securely from my own devices without the SSH tunnel.

1. Install Tailscale on this server and give me the login link so I can connect it
   to my Tailscale account. Wait for my "done".
2. Expose the dashboard over the tailnet ONLY: use `tailscale serve` to put it
   behind HTTPS on the tailnet, proxying to 127.0.0.1:8892.
   HARD RULES: do NOT use `tailscale funnel` (that is public exposure), and do NOT
   change the server bind away from 127.0.0.1. The loopback-only rule from the
   original build stays exactly as it is - Tailscale is a proxy in front of it.
3. Make it survive reboots (tailscale serve config persists; confirm it does).
4. Report back: the Tailscale hostname and 100.x IP, the exact HTTPS URL I open on
   my phone, confirmation the dashboard loads over that URL from a tailnet device,
   and confirmation the public IP still serves NOTHING. All four, with evidence.

Prompt 35

Prompt 35 — The Docs Tab

Back up first. Fill the Docs tab with a clear, self-contained guide to THIS studio:
the crew (who does what), the promote flow step by step (count → write → review/refine →
template → fit preview → render), how fitting to character budgets works and why, the
publish modes and what each does on Buffer, the delivery-truth sync, the calendar, brand
configuration, where the data lives (cadence.db, drafts/, the secret store), and a short
troubleshooting list (agent rate limits, Buffer org id, renderer browser discovery).
Match the template's design language exactly.
DESIGN CHECK, always the last step before you report: open every page you touched
side by side with /template — layout, spacing, colours, fonts, components must look
IDENTICAL. Fix any drift until you cannot tell them apart. Then bump the badge.

Prompt 36

Prompt 36 — Final Verification & Version Stamp

Final check. Run through this list and report each item pass/fail with evidence:

1. Fresh reload: EVERY tab renders with ONLY real data — including Templates,
   Calendar, and Configuration — zero demo values anywhere.
2. Promote one real 3-slide idea end to end: write → review → fit to a PACK template →
   render → appears in Studio with images.
3. Edit one slide's title in Studio → it re-renders with the new text.
4. Change that carousel's template → images visibly update to the new design.
5. /api/templates: built-ins + all three packs present with previews; the picker groups
   by category.
6. Publish flow: correct behaviour for my Buffer state (clear errors OR real channel
   chips; no post created without my confirmation).
7. Calendar + Published: consistent with the drafts' real states.
8. Configuration: handle round-trip proven.
9. Agents tab heatmap matches the runs table; logging still flowing.
10. Server survives a restart (stuck drafts reaped, previews intact, no data loss).
11. DESIGN FIDELITY: walk every tab side by side with /template — the finished dashboard
    must be visually indistinguishable from the original design (layout, spacing,
    colours, fonts, components). Fix any drift you find.

Fix anything that fails (backup first), then stamp the version badge v1.0 and give me a
one-paragraph summary of what my studio can do.

Prompt 37

Prompt 37 — Animated Carousels (Motion)

Back up first. Add motion rendering to the studio.

Build ~/carousel-templates/animate-slides.js (same findBrowser logic as the exporter):
takes <carousel.html> <outDir> [--slides=1,3] [--reel=4:5|9:16|none]; for each selected
.slide, auto-choreograph its REAL markup — every text block and image reveals in reading
order (fade + rise ~42px, ease-out-cubic, staggered ~0.14s apart, capped at 1.6s) — by
tagging elements and exposing a window.__seekSlide(idx, time) that positions the
animation at an exact time. Render each slide frame-by-frame (30 fps, ~2.4s + 1.25s
hold): seek → clipped screenshot → ffmpeg into motion/clip-NN.mp4 encoded at THE SLIDE'S
OWN aspect (1080 wide, height from the slide rect — 4:5 decks give 1350, native-9:16
decks give 1920; yuv420p, faststart). Then the reel: --reel=4:5 → plain concat of the
clips; --reel=9:16 → re-encode each clip over a blurred cover-fill 1080×1920 background,
then concat. Write motion/meta.json {ratio, slides} and print MOTION-DONE JSON.
FOUR HARD-WON ENGINE RULES (each one is a real production bug):
  1) CLEAN SLATE: at start, delete old clip-*.mp4/reel.mp4/meta.json — stale clips from a
     previous run at another ratio must never mix into the new gallery/reel.
  2) FRAMES ON REAL DISK: temp frames live under <outDir>/.frames, NEVER /tmp — a
     10-slide film is ~700 frames and failed runs fill tmpfs until Chromium dies.
     Clean .frames up on failure too.
  3) VIEWPORT ≥ TALLEST SLIDE: with captureBeyondViewport:false, a slide taller than the
     viewport is captured PARTIALLY and then stretched by the encoder (cut bottoms,
     squashed text). Measure max .slide height after load and resize the viewport up.
  4) ROBUST LOAD: ONE goto with waitUntil 'load' (a second goto after a networkidle
     timeout detaches the frame), then a bounded fonts+images settle; freeze any NATIVE
     css animations/transitions with an injected *{animation:none!important} style — the
     seek choreography must be the only motion.

pipeline.animate_draft(draft_id, slides=None, reel_ratio="4:5"): run it on the draft's
carousel.html; ALSO support reel_ratio "9:16-full" — full-frame vertical with NO blur:
  · reflow-capable templates (your flexbox built-ins + AI themes): re-render the deck's
    html at canvas 1080×1920 into carousel-916.html (html-only, never clobber the PNGs)
    and film THAT;
  · fixed-composition pack templates: auto-degrade to the blur 9:16 with a friendly note
    (stretching them strands content at the top);
  · REELS pack templates are ALREADY 1080×1920: film carousel.html as-is, plain-concat
    the reel, keep the 9:16-full label.
Correct meta.json's ratio afterwards; note the result on the draft, log as dev.
server.py: POST /api/draft/animate {draft, slides?, ratio?} (background job + an
"animating" flag), motion info on the draft view: clip URLs + reel URL (mtime-versioned)
PLUS the meta ratio — the UI must render tiles and the reel player at the RIGHT aspect,
never hardcode 4:5.

Studio detail: a PREMIUM two-step visual modal (drawn diagram cards, minimal text — this
is a flagship interaction, not a form):
  Step 1 · what to animate: Everything / Pick slides (tap slides in the grid, yellow
  selection rings + count) / Reel only.
  Step 2 · reel format: 4:5 · full-frame 9:16 (disabled with "not for this design — use
  blur" on fixed packs; "this design is native 9:16" on Reels) · blur 9:16.
When done, a gallery: the stitched reel player + download, each clip as a small looping
video with per-slide download, a ratio badge, a Re-animate button, and a red ✕ Remove
(the media/delete endpoint). Fine-tune tweaks (if any) carry into clips automatically —
the animator films the same carousel.html the tweaks are injected into.

Publish integration: when motion exists, the publish flow asks WHICH format to post —
🖼 static slides / ✨ animated reel (one video) — with plain-language labels, and the
confirmation names the choice. (Video posting to Buffer needs a video host — if none is
configured, show a clear message; static publishing is unaffected.)

Confirm on an existing rendered draft: animate it, play the reel, and show the format
question appearing in the publish flow.
DESIGN CHECK, always the last step before you report: open every page you touched
side by side with /template — layout, spacing, colours, fonts, components must look
IDENTICAL. Fix any drift until you cannot tell them apart. Then bump the badge.

Prompt 38

Prompt 38 — AI Imagery Templates (fal.ai)

Back up first. Add AI-imagery templates. Everything reads FAL_API_KEY from the secret
store; without it, picking one of these templates errors clearly and nothing else breaks.

THEMES — add four new builtin templates in a new "AI Imagery" category, all sharing one
slide layout (full-bleed background image + gradient scrim + kicker / title / body /
@handle + counter footer) but different moods:
  ai-cinema (Dark Cinema) · ivory-studio (bright airy cream) · neon-pop (electric
  purple/cyan gloss) · golden-hour (warm analog film)
Each theme defines its mood sentence and its text-zone rule (dark themes reserve a
near-black bottom 40% for text; light themes a bright one). Slides reference their
artwork at bg/bg-NN.jpg relative to carousel.html.

ART PASS — in pipeline.py:
  art_direction(title, slides, mood): ONE Atlas call returning JSON {style, brands
  (max 2 real products with their GitHub org — single word, validated), cover (concept
  line), scenes (one per content slide that VISUALLY ECHOES that slide's specific
  message — a metaphor for its point, not decoration — tinted with a side's accent
  colour when the deck compares two things)}.
  Cover image: generate via an image-edit model that composes with REAL reference
  images (fal-ai/nano-banana/edit): pass the brands' real GitHub avatar URLs so actual
  logos appear (never let a model draw a logo from memory — it will hallucinate), plus
  a STRICT LAYOUT brief reserving the theme's text zones. 4:5 aspect.
  Content images: fal-ai/flux/dev per scene + the shared style suffix + "no text, no
  letters, no logos".
  ensure_ai_backgrounds(draft_id, slides, template): orchestrates all of it, caches
  everything in drafts/<id>/bg/ (re-renders are FREE — only missing images generate),
  saves art.json alongside for reuse.
  render_draft hooks it: AI template → art pass first, then normal render.

Template previews: seed each AI theme's picker preview from a bundled sample image so
the picker looks right without spending anything.

Confirm structurally WITHOUT generating images if I haven't added a fal key (clear
error path); if I have, ask me before generating ONE test deck (~$0.50).
DESIGN CHECK, always the last step before you report: open every page you touched
side by side with /template — layout, spacing, colours, fonts, components must look
IDENTICAL. Fix any drift until you cannot tell them apart. Then bump the badge.

Prompt 39

Prompt 39 — ImgBB Auto-Cleanup (90-Day Expiration)

Back up first. One small change in pipeline.py: the ImgBB upload must request
auto-deletion after 90 days, so uploaded slide images clean themselves up.

In _imgbb_upload, add the expiration parameter to the upload call:
  https://api.imgbb.com/1/upload?key=<key>&expiration=7776000
(7776000 seconds = 90 days. ImgBB accepts 60 up to 15552000.)

WHY 90 and not less: Buffer fetches the image AT POSTING TIME. A post scheduled two
weeks out must still find its image alive — 90 days outlives any realistic schedule.
Do not make this configurable and do not touch anything else in the function.

Prove it: run one test upload through _imgbb_upload and show me the API response —
it should include the expiration. Then bump the version badge.

Prompt 40

Prompt 40 — Claude Code as Your Dev (Alternative Builder)

You are running on the server where I run my Hermes agent setup. My agents have
already BUILT the first half of a content-automation studio; from now on YOU take over
the construction (the engines, the content API, the remaining wiring). Before touching
anything, get your bearings — explore this machine thoroughly:

- Hermes lives at ~/.hermes — look at its layout: config.yaml, .env (do NOT print any
  secret values), profiles/ (each subfolder is a persistent agent with its own SOUL.md,
  USER.md, memory and workspace), state.db, skills/.
- I have already raised a five-agent content crew in Hermes (Part 1 of this tutorial,
  done in Telegram): the Orchestrator plus four specialists on profiles
  scout (ATLAS), scribe (VERA), dev (KITE), reach (ORIN). Confirm those profiles exist
  and read a couple of their SOUL.md files so you know who they are.
- Hermes may be new to you: READ THE OFFICIAL HERMES DOCS NOW and keep them as your
  reference for the entire build: https://hermes-agent.nousresearch.com/docs
  Study especially how to invoke an agent non-interactively (HERMES_HOME pointing at a
  profile, the -z / --cli flags), because the pipeline you will build calls these agents
  exactly that way. From now on, whenever ANY Hermes question comes up in a later prompt
  (config format, profile layout, CLI behavior, platform settings), consult these docs
  first — never guess about Hermes.
- Study what the crew already built in ~/cadence-dashboard: store.py, server.py (v1),
  the running dashboard at port 8892 (GET / serves it), the installed design
  index.html + /template, and the three template packs in the project root. Read the
  code — you EXTEND it from here, matching its stdlib-only style; you never rewrite it.
- Check the basics of this box too: OS, Python 3, Node (Hermes bundles one at
  ~/.hermes/node/bin), free disk.

Standing rules for the whole build: you BUILD the system (files, servers, wiring); the
Hermes agents RUN the content work — never impersonate them, never edit their SOUL/USER/
memory files, never print secrets from .env, and back up any file you are about to
modify once a backup script exists. SECURITY: the dashboard binds to 127.0.0.1 ONLY and
is reached through the owner's SSH tunnel — never rebind it to 0.0.0.0, never open
firewall ports or reverse proxies for it, on any prompt, ever. Work autonomously, verify each step yourself with
real commands, and report what you actually did.

When you are done exploring, give me a short map of the setup as you understand it and
confirm you are ready to build.

Prompt 41

Prompt 41 — Database Auto-Cleanup (Log Retention)

Back up first. Keep cadence.db from growing forever. The runs table logs every agent
action and piles up endlessly; ideas and drafts are CONTENT and must NEVER be touched
by any cleanup — this script may only ever delete from runs.

Create ~/cadence-dashboard/cleanup-runs.sh (bash + Python stdlib, no pip) that:
  - reads the DB path from CADENCE_DB (default ~/cadence-dashboard/cadence.db),
  - deletes rows in the runs table older than RETENTION_DAYS (set to 30 at the top
    of the script) using the created_at column,
  - touches NOTHING else — no other table, ever,
  - runs VACUUM afterward to actually reclaim the disk space,
  - prints how many rows it deleted and how many remain,
  - is safe to run on a fresh setup (missing db/table = report zero, exit cleanly).
Make it executable.

Schedule it weekly with cron: Sunday 03:00 server time — deliberately BEFORE the
nightly backup window, so the backup that night captures the freshly compacted
database. Append to the existing crontab, never overwrite it, and show me the exact
line you added.

Then run it once now as a test and show me the summary output. Bump the version badge.

Prompt 42

Prompt 42 — Renderer Can't Find a Browser

The exporter's findBrowser() checks: CADENCE_CHROME env var → ~/.cache/ms-playwright →
~/.cache/puppeteer → system chrome/chromium. Diagnose in order:
1. ls ~/.cache/ms-playwright/ — a standard Hermes install puts Chromium here. If empty:
   run `npx playwright install chromium` once (Node from ~/.hermes/node/bin if needed).
2. If Chromium exists but isn't found, print the exact path and set CADENCE_CHROME to it
   in the server's environment, then restart.
3. Remember the server runs under systemd/nohup with a minimal PATH — the service
   environment must include ~/.hermes/node/bin so `node` resolves.

Prompt 43

Prompt 43 — Pack Templates Render in the Wrong Fonts

The packs' fonts_css loads families from Google Fonts at render time. The RENDER machine
must have internet when the exporter runs (networkidle0 waits for the fonts); offline,
every pack template silently falls back to system fonts with different metrics. Check
connectivity from the server's environment, re-render, and compare against the pack's
own sampleHtml preview.

Prompt 44

Prompt 44 — "No buffer accounts are connected" (but they are)

This is almost always a missing BUFFER_ORG_ID. The channels query REQUIRES the
organization id — a valid token alone lists nothing. Fix:
1. Query { account { organizations { id name } } } with the token.
2. For each org id, query channels(input:{organizationId:...}); pick the one returning
   your channels.
3. Save it as BUFFER_ORG_ID in the secret store and restart. The Configuration tab must
   report Buffer by REAL channel discovery, not token presence — if it doesn't, rewire
   that check.

Prompt 45

Prompt 45 — Agent Replies Won't Parse ("copy parse failed")

Three usual causes, in order of likelihood:
1. Model rate limit — the raw output contains a usage-cap error, not JSON. Detect it
   (friendly_error) and show "AI usage limit reached — try again in a bit." instead of
   a parse error. Wait or upgrade the model plan.
2. The agent wrapped JSON in prose/fences and extract_json is too strict — it must try
   fenced blocks first, then brace-match the first valid object anywhere in the text.
3. The prompt drifted — the _JSON_SPEC block must be the LAST thing in the writing
   prompt, and must say "Return ONLY JSON, no prose, no code fences."