content-os-builder · diff
git:20260906.201008b to git:20261003.f7c3bf6
213 added, 100 removed. Audit A to A.
---
name: content-os-builder
- description: Build a personalized "content OS" for any creator/brand — a local dashboard app (Strategy · Ideas · Radar · Carousels · Stories · Presentations · Scripts · Calendar · Gallery · Analytics · Inspiration · Growth · a brand-aware chat Assistant) — every view a real route, built on one machine-readable brand kit that every AI surface reads, a validated + deduplicated idea store with an approve-before-generate review queue, a live trend-radar, an HTML→PNG export engine, a design system, background AI generation, real Instagram/YouTube analytics (posts, insights, comments, inline playback, click-to-transcribe) and a competitor study library. WORKS WITH ANY CODING AGENT (Claude Code, Codex, Kimi, Cursor…). It ALWAYS runs a brand intake interview FIRST (colors, visual identity, platforms, voice, radar topics, no-gos, API credentials, competitors), then generates the whole OS around the answers. Use when someone says "build me a content OS / content system", "make me something like jamal-os", "a dashboard to run my content", "track my posts and competitors", or "an AI content operating system for my brand".
+ description: Build a local AI "content operating system" for one business or many isolated businesses (an agency's clients). One Next.js engine plus one folder per business, with an owner-approved brand kit, an approve-before-generate idea store, a grounded trend radar, competitor tracking, HTML→PNG export, optional AI image/video/animation, a chat assistant with memory, owner-confirmed publishing, a CRM and an optional knowledge-vault graph. Every spawned AI agent is confined (no shell, no access to other businesses). Works with any coding agent (Claude Code, Codex, Cursor, Kimi…). It ALWAYS interviews first, starting with "one business or several isolated ones?", then builds. Use when someone wants a system to run their or their clients' content ("build me a content OS", "something like my friend's content OS", "a dashboard to run my content", "a system to run my clients' content", "an AI content OS for my agency"), even if they never say "content OS".
---
# Content OS Builder
- You are an AI coding agent (Claude Code, Codex, Kimi, Cursor, or similar) with **file-write and shell access**. Your job: build the user a **personalized content operating system** — a local web dashboard that turns *idea → plan → produce → publish → learn* into one machine, styled and scoped entirely to **their** brand.
+ You are an AI coding agent with **file-write and shell access**. You will build the user a local
+ **content operating system**: a dashboard that runs *idea → approve → produce → publish → learn* for
+ one business, or for several businesses kept strictly apart. The user is often an agency running
+ content for clients, or a creator with more than one brand.
- This skill is a **playbook**, not a fixed template. You will INTERVIEW the user, then GENERATE their OS from the answers. Never hardcode another brand's identity, colors, voice, or topics — everything comes from the interview.
+ This skill is a **playbook, not a template**. You interview the user, then build from the answers.
+ Every name, colour, voice rule, competitor and key comes from the interview. Nothing in this skill is
+ anyone's real brand, and nothing you build may contain values the user did not give you.
- > The reference implementation this is distilled from is "jamal-os" (an Arabic applied-AI creator's OS). You are recreating that *architecture*, reskinned for the current user. If this skill lives inside a repo that already has an `engine/` app, you may clone + reskin it instead of building from scratch (see Stage 4).
+ ## What you are building, in one paragraph
+ **One engine plus N business folders.** The engine is `engine/app` (Next.js App Router, React,
+ TypeScript strict, Tailwind v4, pnpm), the trusted scripts it runs, and the CLI tools in
+ `engine/tools`. Each business is a **tenant**: `tenants/<id>/`, holding its manifest, its brand kit,
+ its ideas and pieces, its memory, its skills and its own account keys. Every API route resolves
+ exactly one tenant and reads or writes only inside its folder. **The filesystem is the database**:
+ there is no DB and no ORM. AI work is done by the user's own coding-agent CLI (normally `claude`),
+ spawned headless with `cwd` set to the tenant folder, **with no shell** and with inline deny rules
+ that keep it inside that folder. The agent only writes words and code. The engine does everything
+ that executes: scaffolding, exporting, rendering and publishing. A **single-tenant** install is the
+ same engine with exactly one tenant folder. There is no separate "single-brand" code path.
+
---
## The golden rule: INTERVIEW FIRST, BUILD SECOND
- **Do NOT write a single file until the intake interview is complete and the user has confirmed the summary.** The whole value is personalization. Skipping the interview produces a generic clone, which is a failure.
+ **Write no file until the interview is done and the user has confirmed the summary.** The value of
+ the system is that it is theirs. A generic clone is a failed build.
- Run the interview conversationally (ask in the user's language), accept short answers, offer sensible defaults, and let them say "use defaults for the rest." Then echo a compact summary and get a "yes" before building.
+ - Ask in the user's language, **one group of questions per turn**, with a sensible default proposed
+ for each question so they can answer "yes" or adjust.
+ - They may say **"later"** to any question, or "use defaults for the rest". Record a "later" item as
+ pending and build the part it gates in its disabled state, with a visible "not set up yet" notice.
+ - **Never ask for secrets in the chat.** Tell them which variable goes in which file and let them
+ paste it there. If they paste one anyway, put it in the right file, never echo it back, and tell
+ them to rotate it.
+ - End with a compact summary and get an explicit **yes**.
- The full question set + the `brand-kit.json` schema is in **`references/1-interview.md`**. The condensed intake (ask all of these) is:
+ **The very first question is tenancy:**
- 1. **Identity** — brand/creator name, @handle(s), one line on what you do, niche, primary language (+ any secondary), is the primary language RTL (Arabic/Hebrew/Farsi)?
- 2. **Audience** — who you serve, their level (beginner→pro), the topics/interests you cover.
- 3. **Voice** — tone/register (formal? casual? a specific dialect?), 2–3 do's, 2–3 don'ts, emoji policy, one or two sample lines that "sound like you."
- 4. **Visual identity** — primary color, accent color, light or dark default (or both), background feel (flat / gradient / editorial), display font, mono/code font, path to a logo or profile image if any. (Optional: a second palette for print/booklets.)
- 5. **Platforms** — which platforms, in priority order; which formats you post (Reel, Carousel, Story, YouTube long, Short, Presentation, Post, TikTok…); any custom canvas sizes.
- 6. **Strategy** — your content pillars and their ratio (e.g. 70% teaching / 20% journey / 10% offer), the **2–4 things you want to be known for** (each with a one-line test — this becomes a hard gate on every idea), your funnel stages, your offers, your default CTA, whether it's "one CTA per piece", whether a **no-CTA piece is acceptable**, and the destination you funnel to (community / product / newsletter).
- 7. **Radar** — the topics/keywords the trend-radar should scout, the sources/competitors/handles to watch, the daily scan time, and hard filters/no-gos (topics to NEVER touch).
- 8. **Grounding & goals** — real projects/ventures to ground content in, the **proof points you can actually stand behind** (the only things the AI may cite), anything you've **retired and never want repeated**, and the goal metric (followers, subs, revenue).
- 9. **Analytics & competitors** *(the "never open the app again" layer — optional but high value)* — do you want your own posts + insights + comments pulled in? Which accounts (Instagram Business/Creator? YouTube channel?). Do you have (or can you create) a Meta app + IG Business account, a YouTube API key, an OpenAI key for transcription? Which competitors/creators do you want to study? Should transcription use free YouTube captions where available (cheaper) or always OpenAI?
- 10. **Ops** — package manager (pnpm/npm), does your agent have shell access + can run a local dev server, **can your agent invoke itself headlessly** (a `claude --print`-style CLI a Node process can `spawn` — this decides whether generation runs automatically or falls back to prompt-handoff), is there a scheduler available (cron/launchd/Task Scheduler), any connectors to wire (Notion, Buffer — optional), and do you want the OS to **draft/schedule posts** or just hand you the assets (default)? Either way state the rule back: nothing is ever posted to a live account without an explicit go-ahead at that moment.
+ > Will this system run **one business** (single-tenant) or **several businesses or clients, each
+ > fully isolated** (multi-tenant)?
+ > - **Single-tenant.** One brand. The dashboard opens straight into it.
+ > - **Multi-tenant.** An agency or a person with several brands. Each business gets its own folder:
+ > its own brand kit, ideas, content, memory, chat, publishing channels and keys. Its AI agent can
+ > never read another business's files. A picker at `/` switches between them.
+ >
+ > Both use the same engine, and single-tenant is simply one folder. Adding a business later means
+ > adding a folder; nothing has to be migrated.
- Store every answer in **`content-os/brand-kit.json`** (schema in reference 1). This file is the single source of truth the whole build reads from.
+ The full question set (system questions once, then per-business questions), the defaults and how each
+ answer maps to a file are in **`references/1-interview.md`**. Read it before asking anything.
---
- ## Build stages (after the interview is confirmed)
-
- Work top-to-bottom. **Read the matching reference file before starting a stage**, and **run that
- stage's checkpoint before moving on** — each reference ends with one. A stage that "looks done" but
- fails its checkpoint fails everything built on top of it, three stages later, somewhere unrelated.
- After each stage tell the user what you did in one line.
+ ## What ships as code (`assets/`)
- > Stage numbers and reference-file numbers are **not** the same sequence — references 7 and 8 were
- > added later and slot into the middle. Follow this table, not the filenames.
+ Most of this system you generate from the references. **Eleven files are copied verbatim**, because
+ each one has edge cases that were measured in production (or is a security boundary) and a fresh
+ re-derivation from prose gets it quietly wrong. Copy them unchanged, then edit only the places marked
+ `EDIT:` (owner-language sentences, language data, the publish-tool list). The references explain *why*
+ each one works the way it does; the asset is *how*.
- | Stage | What | Reference |
+ | Asset | Copy to | What it is |
|---|---|---|
- | 0 | Probe capabilities | *(below)* |
- | 1 | Brand kit | `1-interview.md` |
- | 2 | Brand docs + design tokens | `2-brand-and-design.md` |
- | **3** | **⭐ Core libs — strategy renderer, idea store, radar contract** | **`7-core-libs.md`** |
- | 4 | The dashboard app | `3-dashboard-app.md` |
- | 5 | Generation + export engine | `4-builders-and-export.md` |
- | 6 | Radar + calendar | `5-radar-and-calendar.md` |
- | 7 | Publish + Growth | `8-publish-and-growth.md` |
- | 8 | **Assistant (chat)** *(optional but high value)* | **`9-assistant-chat.md`** |
- | 9 | Analytics + Inspiration *(optional)* | `6-analytics-and-inspiration.md` |
- | 10 | Verify + handoff | *(below)* |
-
- **Stage 0 — Prerequisites (probe three INDEPENDENT capabilities, don't assume).** These are separate axes — test each and pick the matching mode:
- 1. **Node ≥ 18 + package manager?** No → **Lite Mode** (files + scripts, no web app).
- 2. **Playwright/Chromium installs?** Try `pnpm dlx playwright install chromium`. Fails → keep the app but **export HTML only** (no PNG rendering).
- 3. **Can your agent invoke itself headlessly?** (a `claude --print`-style non-interactive CLI that Node can `spawn`.) Many can't — Cursor's composer, Kimi/IDE-plugin chats, sandboxes that forbid child processes. Get this from the Ops interview answer. No → keep the app but degrade generation/radar to **prompt-handoff** (reference 3). This is orthogonal to Next.js/Playwright — an agent with both can still lack self-spawn.
+ | `assets/tenant.mjs` | `engine/app/src/lib/tenant.mjs` | Tenant discovery, manifest + per-business `settings` validation, the path-traversal guard, per-tenant env (account keys are never inherited) |
+ | `assets/agent-flags.mjs` | `engine/app/src/lib/agent-flags.mjs` | The only place agent confinement is built: profiles, inline deny rules, the env allowlists |
+ | `assets/proxy.ts` | `engine/app/src/proxy.ts` | The same-origin guard in front of every `/api` route (loopback host, no cross-site requests, JSON or multipart bodies only) |
+ | `assets/export-project.mjs` | `engine/app/scripts/export-project.mjs` | The trusted HTML→PNG exporter with a default-deny request firewall |
+ | `assets/shapes.mjs` | `engine/app/src/lib/shapes.mjs` | The two anti-repetition axes text dedup cannot see (shape and lesson) + starter shapes per kind |
+ | `assets/ideas-core.mjs` | `engine/app/src/lib/ideas-core.mjs` | The idea store: validation against the kit, deterministic dedup, quarantine, atomic writes, the mutex |
+ | `assets/extract-json.mjs` | `engine/app/src/lib/extract-json.mjs` | The one tolerant JSON reader the radar verifier and the ingest share |
+ | `assets/radar-verify.mjs` | `engine/app/src/lib/radar-verify.mjs` | The radar's grounding check: SSRF-guarded fetch, page dates, quote shingles, language gate, unproven numbers |
+ | `assets/proposals.mjs` | `engine/app/src/lib/proposals.mjs` | The chat's unforgeable publish proposals: bound to one message, single use, atomic claim |
+ | `assets/new-tenant.mjs` | `engine/tools/new-tenant.mjs` | Scaffolds a valid, empty, gated tenant folder, all or nothing; `--from` makes a throwaway test copy |
+ | `assets/sync-tenant.mjs` | `engine/tools/sync-tenant.mjs` | Re-applies the engine's skills and settings to existing tenants (run it after editing either) |
- **Stage 1 — Brand kit.** Write `content-os/brand-kit.json` from the interview — **with real ids** on pillars, personas, funnel stages, associations and offers; those exact strings become the validated taxonomy in Stage 3. → `references/1-interview.md`
+ `ideas-core.mjs` imports `./strategy-core.mjs`, which you write from reference 4 §3. Everything else the
+ assets import is another asset.
- **Stage 2 — Brand knowledge base + design tokens.** Generate `content-os/brand/` docs (`identity.md`, `voice.md`, `strategy.md`, `visual-system.md`, `no-gos.md`) and the design-token CSS (colors light/dark, fonts, RTL) from the answers. → `references/2-brand-and-design.md`
+ ---
- **⭐ Stage 3 — The core libs. Build these BEFORE any view.** Four zero-dependency ESM modules in `app/src/lib/`: the **ONE strategy renderer** (`brand-kit.json` → a prompt block per scope, shared by the app *and* the cron), the **idea store** (`ideas.json` — uuids, taxonomy validation, atomic writes, deterministic dedup, statuses), and the **radar output contract + ingest**. Everything downstream imports them. Skip this stage and you will parse markdown, drift five copies of the brand summary, and re-insert the same idea daily. → `references/7-core-libs.md`
+ ## Build order
- **Stage 4 — The dashboard app.** START with the **Stage 3.0 bootstrap** (the exact `create-next-app` command + the Tailwind-**v4** setup — no `tailwind.config.js`) before writing any feature; a weak model that skips it ships an unstyled or non-existent app. Then build the sidebar + views, filesystem-as-database API routes (each try/catch → empty default, and seed the empty data files), dark/light theme, RTL if needed, URL-hash navigation, inline SVG icons (NO emoji in the UI chrome). Ideas is a **review queue** (approve / reject-with-a-reason), and Strategy is **read-only**. → `references/3-dashboard-app.md`
+ Work top to bottom. **Before each stage, read its reference. Before moving on, run that reference's
+ checkpoint.** A stage that "looks done" but fails its checkpoint breaks something three stages later,
+ somewhere unrelated. After each stage, tell the user what you did in one line.
- **Stage 5 — Background AI generation + the export engine.** The Produce views (Carousels/Stories/Presentations) are **one reused component** that shows approved ideas on top and the produced pieces of that collection below, refreshing itself when a job ends — an idea must visibly *become* a piece in the same screen. Wire the `/api/generate` (and `/api/radar`) **background-job** pattern (survives reload, has Stop, reconnects), the Playwright HTML→PNG export engine, the scaffold script, and the per-format builder specs (carousel/story/presentation/script/thumbnail). Generation is **gated on an approved idea** and takes an `idea_id`, never a free string. → `references/4-builders-and-export.md`
+ | # | Stage | Reference | Always? |
+ |---|---|---|---|
+ | 0 | Probe the machine, run the interview, confirm the summary | `1-interview.md` | yes |
+ | 1 | Engine skeleton: repo layout, Next bootstrap, copy ALL the assets, tenancy core, `new-tenant`, the tenant folders | `2-architecture-and-tenancy.md` | yes |
+ | 2 | Security: agent confinement, spawn helper, proxy guard, env isolation, `verify-tenants`; then `sync-tenant --all` | `3-security-and-agents.md` | yes |
+ | 3 | Brand kit (schema, renderer, interview, research draft, approval) and the idea store (+ shape and lesson axes) | `4-brand-kit-and-ideas.md` | yes |
+ | 4 | Dashboard shell + design system: picker, tenant shell, home, strategy, ideas, settings, tokens, RTL, phone layout | `5-dashboard-and-design.md` | yes |
+ | 5 | Production: generation jobs, scaffold, trusted export, «edit», builder skills, product library, the produce/gallery/scripts views; then `sync-tenant --all` | `6-production-and-export.md` | yes (library optional) |
+ | 6 | Media engines: AI image posts, AI video, browser-driven video, Remotion animations | `7-media-engines.md` | optional, per engine |
+ | 7 | Radar + competitors + transcripts + precedent index + own analytics + community post | `8-radar-competitors-and-analytics.md` | radar yes; the rest optional |
+ | 8 | Chat assistant: streamed turns, memory, the agent's actions, interview mode | `9-assistant-and-memory.md` | strongly recommended |
+ | 9 | Calendar, publishing (owner-confirmed), growth loop | `10-publishing-calendar-and-growth.md` | calendar yes; publishing optional |
+ | 10 | CRM + partners (service businesses) | `11-crm-and-partners.md` | optional |
+ | 11 | The owner's knowledge vault ("brain") + 3D graph | `12-brain.md` | optional |
+ | 12 | Routing maps, `CLAUDE.md` / `AGENTS.md` for the engine, every tenant and the vault | `13-routing-maps-and-agents-md.md` | yes |
+ | 13 | Verification: the full checklist, then the handoff | `14-verification.md` | yes |
- **Stage 6 — Radar + interactive calendar.** The Radar **view** is part of this stage, not an afterthought: signals and ideas as **cards with their fields as chips and badges** (reference 5b) — a scan rendered as one paragraph of text is the default failure here. The scout prompt from the strategy renderer + the user's topics/sources/no-gos, emitting **structured JSON against the contract**; the on-demand `/api/radar` route; a scheduled daily run (cron/launchd/Task Scheduler) that ingests through the same code path as the app; and a **health check with a visible banner** — a scheduled job nobody watches fails silently, which is the failure mode that actually happens. Then the Notion-style month calendar backed by a JSON store, with rich items (title, type, platform, pillar, status, description, markdown body). Optionally wire read-only connectors (Notion import) if the user has them. → `references/5-radar-and-calendar.md`
+ Build only the optional stages the interview enabled. A module that is off is **off on the server**
+ (its routes return 404 for that tenant), not just hidden in the sidebar. **Each view is built in the
+ stage that builds its route** (reference 5 §4 lists which); the shell treats a 404 from a route that is
+ off or not built yet as "empty", never as an error.
- **Stage 7 — Publish + Growth (close the loop).** The publish panel (assets + caption + one CTA + a pre-publish checklist), the **three-layer no-auto-publish guarantee** and its verify script, **and — if a connector is wired — media that actually reaches the draft** (a connector runs on remote servers and cannot read a local path or `localhost`; a caption-only draft is a failure, not a partial success). If the connector has **no upload tool** (Buffer does not), you are hosting the files yourself: read reference 8b before writing a line of it — it carries a security rule (never tunnel the dashboard, its generate route runs an agent with permissions skipped) and a failure mode where every server-side signal reports success while the images render broken. then the Growth view — metrics, the weekly review, and hooks that graduate into the bank **with their real number**. Without this the OS produces forever and learns nothing. → `references/8-publish-and-growth.md`
+ ### Stage 0: probe, don't assume
- **Stage 8 — The Assistant (chat) surface** *(optional, but the highest-value view per line of code).* A chat pane that opens with the strategy block, so the user can ask for hooks or a rewrite without re-explaining their brand. Conversations are a real store (`chats.json`), each one a real route (`/assistant/<id>`), replies render as markdown with per-message direction, and the agent runs **with its tools disabled** so it answers instead of announcing it will go read files. → `references/9-assistant-chat.md`
+ Test each of these on its own. They are independent:
+ 1. **Node ≥ 20 and pnpm** (or the package manager the user chose). If Node is missing, ask the user to
+ install it (nothing here runs without Node). If Node works but the Next app cannot be installed,
+ use **Lite Mode** (below).
+ 2. **Symlinks.** Create one in a temp dir (`ln -s` / `fs.symlinkSync`). On Windows without Developer Mode
+ or admin rights this fails: `new-tenant.mjs` then writes `AGENTS.md` as a generated copy and links
+ `node_modules` with a directory junction, and `verify-tenants` accepts both. Recommend WSL when it
+ is available; it behaves like Linux.
+ 3. **The runtime agent CLI.** Can a Node process spawn it headless (`claude --print` style), and is it
+ logged in? Does it support a closed tool list, inline settings with deny rules and a
+ non-interactive permission mode? The confinement layer is written for the `claude` CLI. For any
+ other CLI, read "Adapting to another agent CLI" in reference 3. **If the agent cannot be confined,
+ generation for a multi-tenant system must degrade to prompt-handoff**, because an unconfined agent
+ can read every business.
+ 4. **Optional tools**, only if the interview enabled the module that needs them: `ffmpeg`, `yt-dlp`,
+ the `codex` CLI, Google Chrome.
+ 5. **Playwright Chromium** is probed in the Stage 1 checkpoint, after the bootstrap installs it. If it
+ will not install, use **HTML-only export**.
- **Stage 9 — Analytics + Inspiration + Transcription** *(only if the user gave credentials in Q9 — otherwise skip and say so).* Their own posts with real insights, comments, inline playback, click-to-transcribe; an account-level audience tab; a competitor library with hooks, public numbers, playback and transcripts. **Two deliverables: the grids AND the post detail window that opens on click** — the window is the half that gets skipped, and without it a competitor library teaches nothing (their captions are often just the keyword gate; the value is in the transcript). **Validate every credential live BEFORE building on it** (a dead Meta token is the #1 blocker) and tell the user exactly how to fix any that fail. → `references/6-analytics-and-inspiration.md`
+ ### Degradation matrix (never collapse it into one yes/no)
- **Stage 10 — Verify + handoff.** Typecheck (`tsc --noEmit`) — and prefer typecheck over a full build if the user has a dev server running, since building wipes `.next`. **Never start a dev server to "verify"** — the user runs that. Run the **end-to-end walkthrough** below, then print how to run it (`<pm> dev` in `content-os/app`), where assets land, how the radar scheduler was installed and how to remove it, and remind them to **rotate any API key pasted into the chat**.
+ - **Full.** Node, Playwright and a confinable agent CLI: everything as specified.
+ - **HTML-only export.** Playwright will not install: pieces are written as HTML and previewed as HTML.
+ - **Prompt-handoff generation.** The agent cannot spawn headless, or cannot be confined: Generate,
+ Radar and Edit write the composed prompt to `<piece>/PROMPT.md` and show it; the user runs their
+ agent on it, then presses "I ran it, refresh". Every other part of the dashboard works normally.
+ - **Lite Mode.** Node works but the Next app cannot be installed: build the tenant folders, the
+ zero-dependency core libraries (`tenant.mjs`, `strategy-core.mjs`, `strategy-approve.mjs`,
+ `ideas-core.mjs`, `shapes.mjs`) and their CLI tools: `new-tenant`, `strategy-block`, `approve-kit`
+ (prints the draft's sha256, then approves exactly those bytes) and `ideas` (list / add / approve /
+ reject), all specified in reference 4 §12. Brand approval and the idea store then work from the
+ terminal. Tell the user which parts are app-based and which are file-based.
- ### The Stage 10 walkthrough — do all nine, in order
+ ### Definition of done
- Per-stage checkpoints catch broken parts. This catches a broken *loop*, which is a different failure.
+ You are done only when **reference 14's checklist passes**: typecheck, lint, build, unit tests,
+ `verify-tenants` (with its real-CLI canaries), `verify-publishing-safety` when publishing exists, a
+ real generation from an approved idea, and screenshots of the key screens. **Report honestly which
+ items passed.** Never report a build as done while a failing step goes unmentioned.
- 1. `node tools/strategy-block.mjs radar | head -20` → the brand's real positioning, **not** the fallback
- 2. Run the radar once → `radar/runs/<id>.json` exists, `ideas.json` grew. Run it **again** → the same stories come back **deduplicated**, not inserted
- 2b. **Break it on purpose** — rename `brand-kit.json`, run the cron script: it must abort *before* spending a scan, log a `fail` row, and raise the banner in the UI. Restore, confirm the banner clears
- 3. Try to generate from a `new` idea → **409**. Approve it → generation runs
- 4. The piece exports at the right dimensions, in the brand font, with exactly the CTA the record specified (**or none**, if it was null) — **and it appears in that format's own view without a manual reload**. An empty Carousels tab after a successful job is the #1 build failure: the view was never wired to the filesystem
- 5. The idea flipped to `produced`; the calendar event moves planned → published; the publish panel shows real assets
- 6. If the assistant was built: a starter prompt from the empty state gets a reply (the path that breaks when navigation happens mid-turn), Arabic and English both render un-mangled, and a reload keeps the conversation
- 7. If analytics was built: clicking a card — **your own AND a competitor's** — opens the detail window, plays, and transcribes
- 8. If a publishing connector was wired: a draft made from an 8-slide carousel arrives with **8 images in the right order**, not the caption alone — and **open it in the connector's own web UI**, because an API that reports `attached: 8` can still be showing eight broken thumbnails (reference 8b)
- 9. `node tools/verify-publishing-safety.mjs` exits 0 — and exits 1 when you temporarily move a publish tool into `allow`
+ ---
- Report honestly which of the six passed. **Do not report the build as done with a failing step
- unmentioned** — say which one failed and what you tried.
+ ## Single-tenant vs multi-tenant: what actually changes
- ---
+ | Concern | Multi-tenant | Single-tenant |
+ |---|---|---|
+ | Folder layout | `tenants/<id>/` × N (often the agency's own workspace + one per client) | `tenants/<id>/` × 1 (the same layout, so adding a second business later costs nothing) |
+ | `/` | Picker: one card per business (+ the vault card) | Redirects to `/<only-id>` when exactly one valid tenant exists |
+ | Header | Workspace switcher | Business name only, no dropdown |
+ | Agent confinement, deny rules, no shell | Required | **Still required**: it keeps the agent out of the engine source, the repo-root secrets and the settings |
+ | Account keys | Each tenant's own `.env.local` | Still the tenant's `.env.local`, never the engine env, so a second business can never inherit them |
+ | CLI tools | `--tenant <id>` required, no default | Still required (a shell alias is fine) |
+ | Canaries | Real tenant = victim, throwaway `zz-canary` = attacker | Same: the verifier creates its own attacker |
- ## Degradation matrix (don't collapse these into one yes/no)
+ In the UI a tenant is called a **"workspace"** (in the user's language), never "tenant".
- Capabilities are independent (Stage 0). Pick the highest tier each supports:
- - **Full** — Node + Playwright + self-spawn: the whole OS as described.
- - **HTML-only export** — Node + self-spawn, but Playwright/Chromium won't install: build the app, generation writes the HTML pieces but skips the PNG screenshot step (deliver/preview the HTML).
- - **Prompt-handoff generation** — Node app builds, but the agent can't spawn itself: the Generate/Radar/Edit buttons write the composed prompt to `content-os/<collection>/<slug>/PROMPT.md` (or a modal/clipboard); the user runs their agent on it; an "I ran it — refresh" button re-scans. Dashboard is a full planning/viewing/calendar surface with manual generation. (See reference 3.)
- - **Lite Mode** — no Node app at all (below).
+ ---
- ## Lite Mode (no web app possible)
+ ## Non-negotiables
- If a full web app isn't feasible, still deliver a real content OS as **files + scripts**:
- - `content-os/brand/` knowledge base + `design-tokens.css` (Stage 2, unchanged).
- - **The core libs anyway** (Stage 3 / reference 7) — they are plain ESM with zero dependencies and need no app. `node tools/strategy-block.mjs generate` prints the brand block to paste into any chat, and `ideas.json` + `tools/radar-ingest.mjs` still give you a deduplicated, validated idea store from the CLI. This is the highest-value part of the OS and it survives having no dashboard.
- - Per-format **builder skills** (markdown specs) + self-contained **HTML templates** the agent fills per piece; export to PNG via a headless-browser script *if available*, else deliver the HTML.
- - A **radar** command: a prompt the agent runs (with its own web-search tool) that writes a structured run to `content-os/radar/runs/<id>.json`, ingested by `tools/radar-ingest.mjs`; `radar/feed.md` stays the human log.
- - A **calendar** as `content-os/calendar.json` + a tiny static HTML viewer (single file, no build step).
- - `growth/metrics.md` + the weekly-review prompt (reference 8c) — the learning loop needs no app either.
- Everything else (voice, no-gos, pillars, CTAs) still comes from the interview. Tell the user which parts are file-based vs app-based.
+ Carry every one of these into what you generate. Each has its reason, because a rule without a reason
+ gets "simplified" away.
- ---
+ - **⭐ Personalization only.** Every value comes from the interview. Never leave a value from an
+ example, from this skill, or from another business in code defaults, prompts or templates. A
+ default in code silently brands every tenant whose kit omits that field. Per-business numbers
+ (freshness, thresholds, currency, credit ceilings, the publishing language) live in `tenant.json`
+ `settings`; the only defaults in code are the neutral operational ones the interview proposes
+ (`DEFAULT_SETTINGS`), and anything the owner was not asked stays unset, never guessed.
+ - **⭐ No default tenant, anywhere.** Routes, CLI tools and the cron must name a tenant. A forgotten
+ tenant has to fail loudly; the alternative is silently reading or writing another business's data.
+ - **⭐ No spawned agent ever gets a shell.** A child process such as `node` runs with the operator's
+ full filesystem access, and no CLI flag confines it. This was measured: a tenant agent with a shell
+ read another tenant's files. So the **route** scaffolds before the spawn and runs the trusted
+ exporter or renderer after it. The agent writes words and code; the engine executes. Never emit
+ `--dangerously-skip-permissions`. The one shell-capable helper the skill allows, the optional free
+ Codex image backend, is named as such, opt-in per business, and runs with no keys in a scratch
+ directory outside the repo (reference 7 §1).
+ - **⭐ Confinement is inline and engine-owned.** Pass deny rules as inline `--settings` JSON, never as
+ a path to a settings file the agent can edit. One injected turn could otherwise disarm the next
+ spawn.
+ - **⭐ Agents and renderers get an env allowlist, never the tenant's keys.** Account-bound keys come
+ only from that tenant's `.env.local`. Inheriting the engine env once made every company tenant read
+ the owner's own social audience as its own.
+ - **⭐ The API is local and guarded.** Bind dev to `127.0.0.1`, put the same-origin proxy in front of
+ `/api`, and return 403 from every spawning, publishing or paying route when
+ `NODE_ENV === "production"`. The API has no login, so these are the only things keeping any website
+ the owner visits from driving it, and "the owner's click" means whoever is at this machine: never
+ promise per-person approval.
+ - **⭐ Skills and settings are copied, so they are re-synced.** A tenant gets the engine's skills and
+ settings when it is created; after any edit to `engine/skills/tenant/*`, `PUBLISH_ASK` or the deny
+ lists, run `node engine/tools/sync-tenant.mjs --all`. Agents cannot fix a stale copy themselves.
+ - **⭐ The brand kit is human-controlled.** The AI drafts it and the engine validates it; only the
+ owner's **Approve** click makes it live, and approval signs the exact bytes the owner reviewed (sha256).
+ No agent can write `brand/strategy.json`. Until a kit is approved the tenant is gated: generate and
+ radar return 409.
+ - **⭐ One renderer, read fresh.** `strategy-core.mjs` turns the kit into the prompt block for every
+ AI surface: app routes and cron alike, on every request. Never hand-write a brand summary into a
+ route; five copies drift the first time a pillar changes.
+ - **⭐ Structured data is stored structured.** `ideas.json` is the database (uuids, validated
+ taxonomy, atomic writes, a mutex, deterministic dedup). Markdown files are for humans and are never
+ scraped to build a view.
+ - **⭐ Variety is checked on its own axes.** Text dedup compares wording. `shape_id` (the form an idea
+ takes) and `core_lesson` (what it teaches) are gated separately, per idea and per radar run, because
+ a store can fill with dozens of ideas of one form, or six ideas teaching one lesson, while every
+ similarity rule reports it clean.
+ - **⭐ The engine, not the model, decides what is real.** Every radar idea is re-fetched, dated and
+ quote-checked by code before it reaches the store. The verified stamp is written by the engine
+ only. Competitors are studied from transcripts, never captions.
+ - **⭐ Nothing is produced unreviewed, and nothing is published unconfirmed.** Generation takes the id
+ of an `approved` idea. Publishing happens only on the owner's click in that moment. The chat can
+ only *propose*, into a store the engine owns.
+ - **⭐ Never invent.** No fabricated numbers, results, clients, prices, product details or
+ first-person habits, in code defaults, prompts or content. The proof bank and the product library
+ are the whole list of what may be claimed.
+ - **⭐ Silent failure is the real bug.** Every scheduled or background job reports its health on
+ screen (a banner, not a log file). A failure folder nobody opens is not a safety net.
+ - **⭐ A record with fields renders as fields, and a control that refuses says why.** Use chips,
+ badges and pull-quotes, never a stringified record. A disabled button carries its reason. Show the
+ owner a sentence in their language, never an error code.
+ - **⭐ Isolation is proven, not assumed.** Real-CLI canaries start with a positive control and assert
+ on disk (sha256), never on what the model says.
+ - **RTL is first-class.** Set `dir` once at `<html>`, use logical properties only, never put letter
+ spacing on Arabic, and fix bidi glitches in the content (reference 5).
+ - **Never run the dev server yourself.** Verify with typecheck, lint, build, tests and real runs of
+ the CLI tools. The user starts `pnpm dev` and shows you the result.
- ## Non-negotiables (carry these into whatever you generate)
+ ## How you work while building
- - **Personalization only.** Every color, font, voice rule, pillar, CTA, radar topic, and no-go comes from `brand-kit.json`. No leftover reference-brand values.
- - **⭐ One source of truth, one renderer.** `brand-kit.json` is the only place the brand is defined, and `strategy-block.mjs` is the only thing that turns it into a prompt. Every AI surface — app routes *and* the cron — imports that one renderer and reads the kit fresh per request. **Never hand-write a brand summary into a route.** Five summaries drift silently the first time a pillar changes.
- - **⭐ The kit is human-controlled.** The AI reads it and may propose changes; it must never write it. Serve it GET-only. A system that can rewrite its own guardrails has none.
- - **⭐ Structured data is stored structured.** `ideas.json` is the database; markdown files are research records for humans to read. Never scrape prose to build a view.
- - **⭐ Nothing gets produced unreviewed, and nothing gets published unconfirmed.** Generation requires an idea whose status is `approved`; publishing to a live account requires an explicit go-ahead in that moment, every time.
- - **Never invent.** No fabricated numbers, results, features, or client details — anywhere, in any generated piece. The proof bank in the kit is the whole list of what may be cited. A piece with no proof says less, it doesn't make things up.
- - **Respect the user's no-gos** in the radar prompt, the voice docs, and any generation prompt — as hard filters.
- - **RTL correctness** if the primary language is RTL: set `dir` at the root, use logical CSS properties, never hardcode `dir` per element, never apply negative letter-spacing to Arabic. (See reference 2.)
- - **Local-only AI actions.** Routes that spawn a coding agent or skip permissions must be **guarded to local/dev** (return 403 in production) — they run the user's own agent CLI on their machine. And the dashboard has **no publish endpoint**.
- - **The filesystem is the database.** No DB/ORM. API routes read/write real files under `content-os/` and are non-cached (`force-dynamic` / `no-store`).
- - **⭐ A record with fields renders as FIELDS.** Chips for taxonomy, badges for state, a pull-quote for the hook, a link for the source — never a record stringified into a paragraph. The stores here are deliberately structured (ideas, radar runs, pieces, chats); a view that flattens them back into prose throws away the whole point and cannot be scanned. If you are joining fields with ` — ` or reaching for `JSON.stringify`, the view is wrong.
- - **⭐ Data specified ≠ screen built.** Each reference describes a store and then how to *arrange* what it shows; it is easy to read both and never build the screen in between. The views that get skipped this way, every time: the **post detail window** (ref 6), the **Produce views** (ref 3), and the **Radar view** (ref 5b). Check those three by name before you call a build done.
- - **⭐ A control that refuses must say why.** Every disabled button, skipped guard and early return needs a visible reason. A silently inert control is indistinguishable from a broken backend, and that mistake costs hours of debugging in the wrong layer.
- - **Verify by typecheck/build, not by the dev server.** Never start a long-running dev server to "check" — the user runs that.
+ - Use the user's package manager. Pin versions where a reference pins them.
+ - Generate tests next to the core libraries (`node --test engine/tools/tests/*.test.mjs`) as you
+ write them. The verifier and the canaries are part of the product, not an extra.
+ - Never print, log or commit a secret. Make sure `.gitignore` covers `.env`, `.env.local`,
+ `**/.env.local`, `**/.env.*.local`, `node_modules/`, generated PNG/MP4 and `engine/app/.data/`.
+ - Ask before anything paid (a paid image or video API, transcription, Apify) runs for the first time.
+ - Test on a temporary copy tenant and remove it afterwards: `node engine/tools/new-tenant.mjs zz-test
+ --from <id>` copies the manifest, `brand/` and `library/` (never `.env.local`, chats, ideas or
+ publishing records); `rm -rf tenants/zz-test` when done. Never delete the user's data to clean up.
- Start by reading `references/1-interview.md`, then run the interview.
+ Start by reading `references/1-interview.md`, run Stage 0, then ask the tenancy question.