diff --git a/violet-app-agent/apps/agent/demo/DEMO_SCRIPT.md b/violet-app-agent/apps/agent/demo/DEMO_SCRIPT.md
new file mode 100644
index 000000000..b705148f4
--- /dev/null
+++ b/violet-app-agent/apps/agent/demo/DEMO_SCRIPT.md
@@ -0,0 +1,143 @@
+# Violet App Agent Demo Script
+
+## The Hook (0-10 seconds)
+
+**SHOW THE RESULT FIRST**
+
+```
+[Screen: The finished app - cosmic-observer.localhost:5250/home with 90s nostalgia styling]
+```
+
+**Voice:**
+> "I typed one sentence. 47 seconds later, I had a deployed blog with this styling."
+
+---
+
+## The Problem (10-25 seconds)
+
+**Voice:**
+> "Building apps is slow. Even with Rails, you're still writing migrations,
+> controllers, views, CSS... What if you could just describe what you want?"
+
+```
+[Screen: Split - left side shows traditional Rails workflow (terminal commands),
+right side shows a text input box]
+```
+
+---
+
+## The Demo (25-90 seconds)
+
+### Step 1: Describe Your App
+
+```
+[Screen: Chat UI with user typing]
+```
+
+**Voice:**
+> "Here's the entire prompt: 'Build me a blog called The Cosmic Observer about astronomy'"
+
+**Type in the chat:**
+```
+Build me a blog called The Cosmic Observer about astronomy.
+Make a home page with a hero section.
+```
+
+### Step 2: Watch It Build
+
+```
+[Screen: Show the streaming UI with tool calls appearing]
+```
+
+**Voice:**
+> "The agent figures out what to build. Creates a subdomain.
+> Generates styled pages. Verifies everything works."
+
+**Show checkmarks appearing:**
+- ✓ create_subdomain → cosmic-observer
+- ✓ generate_styled_page → Home with 90s CSS
+- ✓ create_page → /home
+- ✓ verify_page → Success
+
+### Step 3: The Result
+
+```
+[Screen: Navigate to cosmic-observer.localhost:5250/home]
+```
+
+**Voice:**
+> "Cream backgrounds. Georgia serif. Teal and coral accents.
+> This isn't generated garbage - it's designed."
+
+---
+
+## The Tech (90-120 seconds)
+
+**Voice:**
+> "Under the hood: LangGraph orchestrating specialized subagents.
+> A Template Designer that only uses verified Liquid tags - no hallucinated code.
+> Rails runner for direct database access. DHH would approve."
+
+```
+[Screen: Quick flash of architecture diagram]
+```
+
+---
+
+## The CTA (120-150 seconds)
+
+```
+[Screen: Hackathon landing page / graphic]
+```
+
+**Voice:**
+> "Want to build something like this? I'm judging the 'Building with AI' track
+> at the Violet Rails Hackathon.
+>
+> Your challenge: take this agent and make it do something we haven't thought of.
+> Deploy a marketplace. Generate a SaaS. Build the next big thing.
+>
+> Link in description. See you there."
+
+```
+[Screen:
+ VIOLET RAILS HACKATHON
+ "Building with AI" Track
+
+ [Register Now]
+]
+```
+
+---
+
+## Recording Notes
+
+### Equipment
+- Screen recording: OBS or QuickTime
+- Audio: External mic if available
+- Resolution: 1920x1080 minimum
+
+### Pacing
+- Speak FAST, pause between sentences (can edit pauses out)
+- Total runtime: 2-3 minutes max
+- Show, don't tell - let the demo do the work
+
+### Editing Tips
+- Cut all dead air
+- Speed up typing (1.5x)
+- Add subtle zoom on key moments
+- Background music: lo-fi or synthwave (low volume)
+
+### Platforms
+- YouTube: Full version with chapters
+- X/Twitter: 60-second cut (hook + demo + CTA)
+- LinkedIn: 90-second version with text overlay
+
+---
+
+## B-Roll Ideas
+
+1. Terminal commands flying by (speed up)
+2. The 90s nostalgia CSS file scrolling
+3. Tool calls streaming in real-time
+4. Before/after comparison (blank page → styled app)
diff --git a/violet-app-agent/apps/agent/demo/SOCIAL_POSTS.md b/violet-app-agent/apps/agent/demo/SOCIAL_POSTS.md
new file mode 100644
index 000000000..19d3fc6b4
--- /dev/null
+++ b/violet-app-agent/apps/agent/demo/SOCIAL_POSTS.md
@@ -0,0 +1,210 @@
+# Social Media Posts for Violet App Agent Demo
+
+## Twitter/X Thread (Main Post)
+
+### Tweet 1 (The Hook)
+```
+I typed one sentence.
+
+47 seconds later, I had a deployed blog with:
+- Cream backgrounds
+- Georgia serif typography
+- Teal/coral CTAs
+- Working navigation
+
+Here's how 👇
+```
+
+### Tweet 2 (The Input)
+```
+The entire prompt:
+
+"Build me a blog called The Cosmic Observer about astronomy. Make a home page with a hero section."
+
+That's it. No YAML. No configs. No boilerplate.
+```
+
+### Tweet 3 (The Tech)
+```
+Under the hood:
+
+• LangGraph orchestrating specialized subagents
+• Template Designer with verified Liquid tags (no hallucinated code)
+• Rails runner for direct DB access
+• 90s nostalgia CSS baked in
+
+DHH would approve.
+```
+
+### Tweet 4 (The CTA)
+```
+Want to build something like this?
+
+I'm judging the "Building with AI" track at the Violet Rails Hackathon.
+
+Your challenge: Take this agent and make it do something we haven't thought of.
+
+Register: [LINK]
+
+See you there.
+```
+
+---
+
+## LinkedIn Post
+
+```
+One sentence. One deployed app.
+
+I've been working on an AI agent that turns natural language descriptions into working Rails applications.
+
+The result? Type "Build me a blog about astronomy" and get:
+✓ A subdomain provisioned
+✓ Styled pages with 90s nostalgia CSS
+✓ Working navigation and forms
+✓ Verified Liquid templates (no hallucinated code)
+
+The secret sauce: A Template Designer subagent that only uses tags that actually exist in the framework. No runtime errors. No broken pages.
+
+Tech stack:
+• LangGraph for agent orchestration
+• Specialized subagents for architecture, CMS, security
+• Rails runner for direct database access
+• Deterministic template system
+
+I'm judging the "Building with AI" track at the upcoming Violet Rails Hackathon.
+
+If you can extend this agent to do something unexpected - deploy a marketplace, generate a SaaS, build the next big thing - I want to see it.
+
+Link to demo and registration in comments.
+
+#AI #Rails #DevTools #BuildInPublic
+```
+
+---
+
+## YouTube Short Script (60 seconds)
+
+```
+[0-5s] HOOK
+"One sentence. 47 seconds. A deployed app."
+
+[5-15s] SHOW THE RESULT
+[Screen: Navigate to cosmic-observer.localhost with styled blog]
+"Look at this. Cream backgrounds. Georgia serif. Teal and coral buttons. This isn't generated garbage."
+
+[15-30s] THE INPUT
+[Screen: Chat UI with typing]
+"Here's the entire prompt: Build me a blog called The Cosmic Observer about astronomy."
+[Screen: Tool calls appearing]
+"Watch. Subdomain created. Page generated. Verified. Done."
+
+[30-45s] THE TECH
+"The secret? A Template Designer that only uses Liquid tags that actually exist. No hallucinated code. No NoMethodError at runtime."
+
+[45-60s] CTA
+"I'm judging the Building with AI track at the Violet Rails Hackathon. Take this agent. Make it do something we haven't thought of. Link below."
+```
+
+---
+
+## Short Tweet (Single Post)
+
+```
+Built an AI agent that turns "Build me a blog about astronomy" into a deployed Rails app in 47 seconds.
+
+Template Designer only uses verified Liquid tags - no hallucinated code.
+
+Judging "Building with AI" at Violet Rails Hackathon.
+
+What would you build?
+
+[DEMO LINK]
+```
+
+---
+
+## Reddit Post (r/rails, r/webdev, r/artificial)
+
+**Title:** I built an AI agent that deploys Rails apps from natural language - 47 seconds from prompt to styled blog
+
+**Body:**
+```
+Been working on this for a few weeks and wanted to share.
+
+**The Problem:**
+AI agents that generate code tend to hallucinate. They'll write Liquid tags that don't exist, causing runtime errors on pages they create.
+
+**The Solution:**
+A "Template Designer" subagent that:
+1. Only uses verified Liquid tags from a whitelist
+2. Generates styled HTML with CSS baked in (90s nostalgia aesthetic)
+3. Verifies every page renders after creation
+
+**Demo:**
+Type: "Build me a blog called The Cosmic Observer about astronomy"
+Get: A working subdomain with styled home page in under a minute
+
+**Tech:**
+- LangGraph for agent orchestration
+- Specialized subagents (Architect, CMS Designer, Security, Deployer)
+- Rails runner for direct database operations
+- Deterministic template system
+
+**Why I'm posting:**
+I'm judging the "Building with AI" track at the Violet Rails Hackathon. Looking for people who want to extend this - deploy marketplaces, generate SaaS apps, whatever.
+
+Code is open source: [GITHUB LINK]
+Demo: [DEMO LINK]
+
+Would love feedback from the Rails community.
+```
+
+---
+
+## HN Post
+
+**Title:** Show HN: AI agent that deploys Rails apps from natural language (no hallucinated code)
+
+```
+I built an AI agent that turns "Build me a blog about astronomy" into a deployed Rails app.
+
+The key innovation: A "Template Designer" subagent that only uses verified Liquid tags. No hallucinated code = no runtime errors.
+
+How it works:
+1. You describe your app in plain English
+2. Agent creates subdomain, generates styled pages, verifies they render
+3. You get a working app with 90s nostalgia CSS (cream backgrounds, Georgia serif, teal/coral accents)
+
+Under 1 minute from prompt to deployed app.
+
+Tech: LangGraph, specialized subagents, Rails runner for DB access.
+
+Open source. Looking for feedback and people who want to extend it.
+
+Demo: [LINK]
+GitHub: [LINK]
+```
+
+---
+
+## Recording Checklist
+
+Before recording the video demo:
+
+- [ ] Clean browser (no tabs, incognito mode)
+- [ ] Fresh subdomain name ready (test first that it doesn't exist)
+- [ ] LangGraph server running and healthy
+- [ ] Rails server running
+- [ ] Screen recording software ready (OBS/QuickTime)
+- [ ] External mic connected (if available)
+- [ ] Script printed or on second monitor
+- [ ] Keyboard shortcuts memorized
+- [ ] Energy drink consumed (speak fast!)
+
+Post-recording:
+- [ ] Trim dead air
+- [ ] Speed up typing to 1.5x
+- [ ] Add zoom on key moments
+- [ ] Export at 1080p minimum
+- [ ] Thumbnail: split screen (chat input | styled result)
diff --git a/violet-app-agent/apps/agent/demo/demo-landing-page.png b/violet-app-agent/apps/agent/demo/demo-landing-page.png
new file mode 100644
index 000000000..0a9446b07
Binary files /dev/null and b/violet-app-agent/apps/agent/demo/demo-landing-page.png differ
diff --git a/violet-app-agent/apps/agent/demo/index.html b/violet-app-agent/apps/agent/demo/index.html
new file mode 100644
index 000000000..8c90bd137
--- /dev/null
+++ b/violet-app-agent/apps/agent/demo/index.html
@@ -0,0 +1,712 @@
+
+
+
+
+
+ Violet App Agent | Build Apps with Natural Language
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ One sentence.
+ One deployed app.
+
+
+ Describe your app in plain English. Get a styled, working Rails app in under a minute.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Agent Workflow
+
+ ✓
+ create_subdomain
+ cosmic-observer
+ 2.3s
+
+
+ ✓
+ generate_styled_page
+ Home + 90s CSS
+ 1.8s
+
+
+ ✓
+ create_page
+ /home
+ 0.9s
+
+
+ ✓
+ verify_page
+ 200 OK
+ 0.4s
+
+
+
+
+
+
+
Result: cosmic-observer.localhost
+
+
+ https://cosmic-observer.yourdomain.com/home
+
+
+
The Cosmic Observer
+
Exploring the wonders of the universe
+
+
+
+
+
+
+
+
+
+
+
+
Built for developers who value their time
+
+
+
💬
+
Natural Language
+
Describe what you want in plain English. No YAML configs. No boilerplate.
+
+
+
🎨
+
90s Nostalgia CSS
+
Cream backgrounds. Georgia serif. Teal and coral accents. Designed, not generated.
+
+
+
✓
+
Verified Templates
+
Only uses Liquid tags that actually exist. No hallucinated code. No runtime errors.
+
+
+
🤖
+
LangGraph Agents
+
Specialized subagents for architecture, CMS design, security, and deployment.
+
+
+
🛤
+
Rails Runner
+
Direct database access via Rails runner. DHH-approved patterns.
+
+
+
🚀
+
Deploy Anywhere
+
Local, GitHub, or Heroku. One command deployment with GitHub Actions.
+
+
+
+
+
+
+
+
+
Coming Soon
+
Violet Rails Hackathon
+
+ Take this agent and make it do something we haven't thought of.
+ Deploy a marketplace. Generate a SaaS. Build the next big thing.
+
+
+
+
+
Building with AI
+
Extend the agent's capabilities
+
+
+
Best UI/UX
+
Most polished user experience
+
+
+
Most Creative
+
Unexpected use cases
+
+
+
+
+ I'll be judging the "Building with AI" track. Show me what you've got.
+
+
+
Register for Hackathon
+
+
+
+
+
+
+
diff --git a/violet-app-agent/apps/agent/e2e_screenshots/eval-01-landing-page.png b/violet-app-agent/apps/agent/e2e_screenshots/eval-01-landing-page.png
new file mode 100644
index 000000000..c39f7c37b
Binary files /dev/null and b/violet-app-agent/apps/agent/e2e_screenshots/eval-01-landing-page.png differ
diff --git a/violet-app-agent/apps/agent/e2e_screenshots/eval-02-home-page.png b/violet-app-agent/apps/agent/e2e_screenshots/eval-02-home-page.png
new file mode 100644
index 000000000..ca6b1b98f
Binary files /dev/null and b/violet-app-agent/apps/agent/e2e_screenshots/eval-02-home-page.png differ
diff --git a/violet-app-agent/apps/agent/e2e_screenshots/eval-03-stories-empty.png b/violet-app-agent/apps/agent/e2e_screenshots/eval-03-stories-empty.png
new file mode 100644
index 000000000..f9be8b120
Binary files /dev/null and b/violet-app-agent/apps/agent/e2e_screenshots/eval-03-stories-empty.png differ
diff --git a/violet-app-agent/apps/agent/e2e_screenshots/eval-04-toronto-page.png b/violet-app-agent/apps/agent/e2e_screenshots/eval-04-toronto-page.png
new file mode 100644
index 000000000..24ffe4be2
Binary files /dev/null and b/violet-app-agent/apps/agent/e2e_screenshots/eval-04-toronto-page.png differ
diff --git a/violet-app-agent/apps/agent/e2e_screenshots/eval-05-jersey-city-page.png b/violet-app-agent/apps/agent/e2e_screenshots/eval-05-jersey-city-page.png
new file mode 100644
index 000000000..b46a12c2b
Binary files /dev/null and b/violet-app-agent/apps/agent/e2e_screenshots/eval-05-jersey-city-page.png differ
diff --git a/violet-app-agent/apps/agent/e2e_screenshots/eval-06-write-error.png b/violet-app-agent/apps/agent/e2e_screenshots/eval-06-write-error.png
new file mode 100644
index 000000000..b705b74a0
Binary files /dev/null and b/violet-app-agent/apps/agent/e2e_screenshots/eval-06-write-error.png differ
diff --git a/violet-app-agent/apps/agent/src/violet_app_agent/agent.py b/violet-app-agent/apps/agent/src/violet_app_agent/agent.py
index c697deab6..31e30ef9f 100644
--- a/violet-app-agent/apps/agent/src/violet_app_agent/agent.py
+++ b/violet-app-agent/apps/agent/src/violet_app_agent/agent.py
@@ -20,6 +20,12 @@
content_researcher_subagent,
deployer_subagent,
security_subagent,
+ template_designer_subagent,
+ list_templates,
+ select_template,
+ get_liquid_tag,
+ generate_styled_page,
+ verify_page,
)
from violet_app_agent.tools import (
create_namespace,
@@ -62,6 +68,12 @@ def create_violet_app_agent(
create_namespace,
# CMS and pages
create_page,
+ # Template Designer tools (deterministic, verified Liquid tags)
+ list_templates,
+ select_template,
+ get_liquid_tag,
+ generate_styled_page,
+ verify_page,
# Deployment
trigger_deployment,
],
@@ -71,6 +83,7 @@ def create_violet_app_agent(
content_researcher_subagent,
deployer_subagent,
security_subagent,
+ template_designer_subagent, # Deterministic template generation
],
system_prompt=SYSTEM_PROMPT,
checkpointer=True if (use_memory and not running_in_langgraph_api) else None,
diff --git a/violet-app-agent/apps/agent/src/violet_app_agent/prompts.py b/violet-app-agent/apps/agent/src/violet_app_agent/prompts.py
index 3b5a4ea3d..4c369d0cf 100644
--- a/violet-app-agent/apps/agent/src/violet_app_agent/prompts.py
+++ b/violet-app-agent/apps/agent/src/violet_app_agent/prompts.py
@@ -96,7 +96,27 @@
1. Create subdomain
2. Create each API namespace
3. Generate forms
-4. Create CMS pages
+4. Create styled CMS pages with 90s nostalgia design
+
+**CRITICAL: For ALL pages, use the Template Designer workflow:**
+```
+1. generate_styled_page(page_type, slot_values, nav_links)
+ → Returns styled HTML with 90s nostalgia CSS
+
+2. create_page(subdomain, title, slug, page_type="custom", content=styled_html)
+ → Creates the page in CMS
+
+3. verify_page(subdomain, slug)
+ → Confirms the page renders without errors
+```
+
+**90s Nostalgia Style** (default for all pages):
+- Cream backgrounds (#fdf6e3)
+- Georgia serif typography
+- Teal/coral accent colors
+- Paper-white content cards
+- Generous whitespace
+- CSS Grid layouts
Report progress as you go.
@@ -168,6 +188,46 @@
- Deploy to production via GitHub
```
+## Template Designer Page Types
+
+Use `generate_styled_page` with these page types:
+
+| Page Type | Use For | Key Slots |
+|-----------|---------|-----------|
+| home | Landing page with hero | site_title, headline, tagline, main_content |
+| category | Topic/category landing | headline, tagline, main_content |
+| post_index | List all posts | headline, tagline |
+| post_show | Single post view | headline, main_content |
+| write | Form submission page | headline, tagline (uses render_form) |
+| about | About/info page | headline, main_content |
+
+**Example: Creating a styled home page**
+```python
+# 1. Generate styled HTML
+result = generate_styled_page(
+ page_type="home",
+ slot_values={
+ "site_title": "The Woodchuck Inquirer",
+ "headline": "How Much Wood?",
+ "tagline": "Exploring life's great questions",
+ "main_content": "Welcome to our blog...
"
+ }
+)
+styled_html = result["html"]
+
+# 2. Create the page
+create_page(
+ subdomain="woodchuck-studies",
+ title="Home",
+ slug="home",
+ page_type="custom",
+ content=styled_html
+)
+
+# 3. Verify it works
+verify_page(subdomain="woodchuck-studies", path="/home")
+```
+
## Property Types
| Type | Use For | Example |
diff --git a/violet-app-agent/apps/agent/src/violet_app_agent/subagents/__init__.py b/violet-app-agent/apps/agent/src/violet_app_agent/subagents/__init__.py
index 41b55ca59..e1b61566c 100644
--- a/violet-app-agent/apps/agent/src/violet_app_agent/subagents/__init__.py
+++ b/violet-app-agent/apps/agent/src/violet_app_agent/subagents/__init__.py
@@ -5,6 +5,17 @@
from violet_app_agent.subagents.content_researcher import content_researcher_subagent
from violet_app_agent.subagents.deployer import deployer_subagent
from violet_app_agent.subagents.security import security_subagent
+from violet_app_agent.subagents.template_designer import (
+ template_designer_subagent,
+ list_templates,
+ select_template,
+ get_liquid_tag,
+ generate_styled_page,
+ verify_page,
+ VERIFIED_LIQUID_TAGS,
+ FORBIDDEN_TAGS,
+ BLOG_TEMPLATES,
+)
__all__ = [
"architect_subagent",
@@ -12,4 +23,14 @@
"content_researcher_subagent",
"deployer_subagent",
"security_subagent",
+ # Template Designer subagent and tools
+ "template_designer_subagent",
+ "list_templates",
+ "select_template",
+ "get_liquid_tag",
+ "generate_styled_page",
+ "verify_page",
+ "VERIFIED_LIQUID_TAGS",
+ "FORBIDDEN_TAGS",
+ "BLOG_TEMPLATES",
]
diff --git a/violet-app-agent/apps/agent/src/violet_app_agent/subagents/template_designer.py b/violet-app-agent/apps/agent/src/violet_app_agent/subagents/template_designer.py
new file mode 100644
index 000000000..c7f6ad0ee
--- /dev/null
+++ b/violet-app-agent/apps/agent/src/violet_app_agent/subagents/template_designer.py
@@ -0,0 +1,548 @@
+"""
+Template Designer Subagent
+
+Following Deep Agent architecture: DIAGNOSE -> MULTI-EXPERT -> ARTIFACT-FIRST -> DOMAIN-NATIVE
+
+This subagent is responsible for:
+1. SELECT appropriate templates from the verified template library
+2. CUSTOMIZE templates with user-specific content
+3. APPLY styling (90s nostalgia CSS by default)
+4. VERIFY each page renders successfully after creation
+
+CRITICAL: Only uses verified Liquid tags that exist in Violet Rails.
+"""
+
+import os
+import requests
+from pathlib import Path
+from typing import Any
+
+from langchain_core.tools import tool
+from langgraph.prebuilt import create_react_agent
+from langchain_anthropic import ChatAnthropic
+
+
+# Template directory path
+TEMPLATES_DIR = Path(__file__).parent.parent / "templates"
+
+
+# Verified Liquid tags that exist in Violet Rails
+VERIFIED_LIQUID_TAGS = {
+ # Content tags (SAFE - always work)
+ "text": "{{ cms:text {identifier} }}",
+ "markdown": "{{ cms:markdown {identifier} }}",
+ "wysiwyg": "{{ cms:wysiwyg {identifier} }}",
+
+ # Asset tags (SAFE)
+ "asset": "{{ cms:asset {identifier}, as: image }}",
+ "file": "{{ cms:file {identifier} }}",
+
+ # Snippet tags (SAFE - if snippet exists)
+ "snippet": "{{ cms:snippet {identifier} }}",
+
+ # Collection tags (SAFE)
+ "collection": "{{ cms:collection {identifier} | namespace: '{namespace}' }}",
+
+ # Form tags (USE render_form NOT render_api_form)
+ "form": "{{ render_form | namespace: '{namespace}' | submit_text: '{submit_text}' }}",
+}
+
+# Tags that DO NOT exist - agent must never use these
+FORBIDDEN_TAGS = {
+ "render_api_form": "Does not exist - use render_form instead",
+ "api_resource": "Does not exist",
+ "render_api": "Does not exist",
+}
+
+
+# Blog templates library
+BLOG_TEMPLATES = {
+ "home": {
+ "name": "Blog Home Page",
+ "file": "blog/home.liquid",
+ "slots": {
+ "site_title": "Site title displayed in browser tab",
+ "headline": "Main headline (h1)",
+ "tagline": "Subtitle under headline",
+ "footer_text": "Footer copyright/attribution",
+ },
+ "description": "Homepage with hero section, category cards, and featured posts",
+ },
+ "category": {
+ "name": "Category Page",
+ "file": "blog/category.liquid",
+ "slots": {
+ "category_title": "Category name (h1)",
+ "category_description": "Category description",
+ "category_content": "Main content area",
+ },
+ "description": "Category landing page with description and posts",
+ },
+ "post_index": {
+ "name": "Posts Index",
+ "file": "blog/post_index.liquid",
+ "slots": {
+ "page_title": "Page title",
+ "intro_text": "Introduction paragraph",
+ },
+ "description": "List all posts with filtering options",
+ },
+ "post_show": {
+ "name": "Single Post",
+ "file": "blog/post_show.liquid",
+ "slots": {
+ "post_title": "Post title",
+ "post_content": "Full post content",
+ "post_author": "Author name",
+ "post_date": "Publication date",
+ },
+ "description": "Individual post view with full content",
+ },
+ "write": {
+ "name": "Submit Story Form",
+ "file": "blog/write.liquid",
+ "slots": {
+ "page_title": "Page title",
+ "intro_text": "Introduction/instructions",
+ },
+ "form_namespace": "stories",
+ "form_submit_text": "Share Your Story",
+ "description": "Story submission form - uses render_form (NOT render_api_form)",
+ },
+ "about": {
+ "name": "About Page",
+ "file": "blog/about.liquid",
+ "slots": {
+ "page_title": "Page title",
+ "about_content": "About section content",
+ },
+ "description": "About page with author/site information",
+ },
+}
+
+
+TEMPLATE_SUBAGENT_PROMPT = """You are the Template Designer subagent for the Violet App Agent.
+
+Your role is to:
+1. SELECT appropriate templates from the verified template library
+2. CUSTOMIZE templates with user-specific content
+3. APPLY styling (90s nostalgia CSS by default)
+4. VERIFY each page renders successfully after creation
+
+## CRITICAL RULES
+
+1. **ONLY use Liquid tags from VERIFIED_LIQUID_TAGS**
+ - text, markdown, wysiwyg: Content display
+ - asset, file: Media
+ - snippet: Reusable content blocks
+ - collection: Data collections
+ - form: User forms (render_form)
+
+2. **NEVER use these tags (they don't exist):**
+ - render_api_form (DOES NOT EXIST - use render_form)
+ - api_resource (DOES NOT EXIST)
+ - render_api (DOES NOT EXIST)
+
+3. **ALWAYS verify pages after creation**
+ - Use verify_page tool after every create_page
+ - Report errors immediately
+ - Do not mark complete until verified
+
+4. **Use 90s nostalgia CSS by default**
+ - Cream backgrounds, paper-white cards
+ - Georgia serif typography
+ - Teal/coral accents
+ - CSS Grid layout
+ - Generous whitespace
+
+## Available Templates
+
+- home: Homepage with hero, categories, featured posts
+- category: Category landing page
+- post_index: All posts listing
+- post_show: Single post view
+- write: Story submission form (uses render_form, NOT render_api_form)
+- about: About page
+
+## Example Workflow
+
+1. User wants a home page -> select_template("home")
+2. Fill slots with content
+3. Apply 90s nostalgia CSS
+4. Create page via apply_template
+5. Verify page renders -> verify_page
+6. Report success or errors
+"""
+
+
+def load_css() -> str:
+ """Load the 90s nostalgia CSS file."""
+ css_path = TEMPLATES_DIR / "styles" / "90s_nostalgia.css"
+ if css_path.exists():
+ return css_path.read_text()
+ return ""
+
+
+def validate_liquid_content(content: str) -> tuple[bool, list[str]]:
+ """
+ Validate Liquid content only uses known tags.
+
+ Returns:
+ (is_valid, list of errors)
+ """
+ errors = []
+
+ for forbidden, reason in FORBIDDEN_TAGS.items():
+ if forbidden in content:
+ errors.append(f"Forbidden tag '{forbidden}': {reason}")
+
+ return len(errors) == 0, errors
+
+
+@tool
+def list_templates() -> dict[str, Any]:
+ """
+ List all available blog templates with their descriptions.
+
+ Returns:
+ Dictionary of template names to their metadata
+ """
+ return {
+ name: {
+ "name": t["name"],
+ "description": t["description"],
+ "slots": list(t["slots"].keys()),
+ }
+ for name, t in BLOG_TEMPLATES.items()
+ }
+
+
+@tool
+def select_template(page_type: str) -> dict[str, Any]:
+ """
+ Select the appropriate template for a page type.
+
+ Args:
+ page_type: One of 'home', 'category', 'post_index', 'post_show', 'write', 'about'
+
+ Returns:
+ Template definition with content slots and requirements
+ """
+ if page_type not in BLOG_TEMPLATES:
+ return {
+ "error": f"Unknown page type: {page_type}",
+ "available": list(BLOG_TEMPLATES.keys()),
+ }
+
+ template = BLOG_TEMPLATES[page_type]
+ result = {
+ "page_type": page_type,
+ "template_name": template["name"],
+ "template_file": template["file"],
+ "slots": template["slots"],
+ "description": template["description"],
+ "style": "90s_nostalgia.css",
+ }
+
+ # Add form info if applicable
+ if "form_namespace" in template:
+ result["form"] = {
+ "namespace": template["form_namespace"],
+ "submit_text": template["form_submit_text"],
+ "liquid_tag": f"{{{{ render_form | namespace: '{template['form_namespace']}' | submit_text: '{template['form_submit_text']}' }}}}",
+ "warning": "NEVER use render_api_form - it does not exist!",
+ }
+
+ return result
+
+
+@tool
+def get_liquid_tag(tag_type: str, identifier: str = "", namespace: str = "", submit_text: str = "Submit") -> dict[str, Any]:
+ """
+ Get the correct Liquid tag syntax for a given tag type.
+
+ Args:
+ tag_type: Type of tag (text, markdown, collection, form, etc.)
+ identifier: Content identifier
+ namespace: Namespace for collection/form tags
+ submit_text: Button text for form tags
+
+ Returns:
+ The correct Liquid syntax to use
+ """
+ if tag_type not in VERIFIED_LIQUID_TAGS:
+ return {
+ "error": f"Unknown tag type: {tag_type}",
+ "available": list(VERIFIED_LIQUID_TAGS.keys()),
+ }
+
+ template = VERIFIED_LIQUID_TAGS[tag_type]
+ result = template.format(
+ identifier=identifier,
+ namespace=namespace,
+ submit_text=submit_text,
+ )
+
+ return {
+ "tag_type": tag_type,
+ "syntax": result,
+ "warning": "Never use render_api_form - it does not exist!" if tag_type == "form" else None,
+ }
+
+
+@tool
+def verify_page(subdomain: str, path: str) -> dict[str, Any]:
+ """
+ Verify a page was created successfully and renders without errors.
+
+ CRITICAL: Call this after EVERY page creation to ensure it works.
+
+ Args:
+ subdomain: The subdomain name (e.g., 'conscious-observer')
+ path: The page path (e.g., '/home')
+
+ Returns:
+ Verification results including success status and any errors
+ """
+ # Construct URL - handle path with or without leading slash
+ if not path.startswith("/"):
+ path = f"/{path}"
+
+ url = f"http://{subdomain}.localhost:5250{path}"
+
+ try:
+ response = requests.get(url, timeout=10)
+
+ # Check for known error patterns
+ has_error = any(err in response.text for err in [
+ "NoMethodError",
+ "NameError",
+ "SyntaxError",
+ "undefined method",
+ "ActionView::Template::Error",
+ "Action Controller: Exception",
+ ])
+
+ # Check for specific forbidden tag errors
+ forbidden_tag_errors = []
+ for tag in FORBIDDEN_TAGS:
+ if tag in response.text:
+ forbidden_tag_errors.append(f"Page uses forbidden tag: {tag}")
+
+ return {
+ "success": response.status_code == 200 and not has_error,
+ "url": url,
+ "status_code": response.status_code,
+ "has_content": len(response.text) > 100,
+ "has_error": has_error,
+ "forbidden_tag_errors": forbidden_tag_errors,
+ "content_length": len(response.text),
+ "recommendation": "Page OK" if (response.status_code == 200 and not has_error) else "Fix errors before proceeding",
+ }
+
+ except requests.exceptions.ConnectionError:
+ return {
+ "success": False,
+ "url": url,
+ "error": "Connection refused - is the Rails server running?",
+ "recommendation": "Start Rails server: bin/rails server -p 5250",
+ }
+ except requests.exceptions.Timeout:
+ return {
+ "success": False,
+ "url": url,
+ "error": "Request timed out",
+ "recommendation": "Check server status and try again",
+ }
+ except Exception as e:
+ return {
+ "success": False,
+ "url": url,
+ "error": str(e),
+ "recommendation": "Investigate the error",
+ }
+
+
+@tool
+def generate_styled_page(
+ page_type: str,
+ slot_values: dict[str, str],
+ nav_links: list[dict[str, str]] | None = None,
+) -> dict[str, Any]:
+ """
+ Generate a complete styled page HTML with 90s nostalgia CSS.
+
+ Args:
+ page_type: Template type (home, category, write, etc.)
+ slot_values: Dict mapping slot names to content values
+ nav_links: Optional list of nav links [{"text": "Home", "href": "/"}]
+
+ Returns:
+ Complete HTML ready to be used as page content
+ """
+ if page_type not in BLOG_TEMPLATES:
+ return {
+ "error": f"Unknown page type: {page_type}",
+ "available": list(BLOG_TEMPLATES.keys()),
+ }
+
+ template = BLOG_TEMPLATES[page_type]
+ css = load_css()
+
+ # Default navigation
+ if nav_links is None:
+ nav_links = [
+ {"text": "Home", "href": "/home"},
+ {"text": "Stories", "href": "/stories"},
+ {"text": "Write", "href": "/write"},
+ ]
+
+ nav_html = "\n".join(
+ f' {link["text"]}'
+ for link in nav_links
+ )
+
+ # Get slot values with defaults
+ site_title = slot_values.get("site_title", "My Blog")
+ headline = slot_values.get("headline", "Welcome")
+ tagline = slot_values.get("tagline", "")
+ main_content = slot_values.get("main_content", "")
+ footer_text = slot_values.get("footer_text", "Made with Violet Rails")
+
+ # Generate page-type specific content
+ if page_type == "home":
+ content_section = f"""
+
+ {headline}
+ {tagline}
+
+
+
+
+ Explore
+
+ {main_content}
+
+
+"""
+ elif page_type == "write":
+ # CRITICAL: Use render_form, NOT render_api_form
+ form_namespace = template.get("form_namespace", "stories")
+ form_submit = template.get("form_submit_text", "Submit")
+ content_section = f"""
+
+ {headline}
+ {tagline}
+
+ {{{{ render_form | namespace: '{form_namespace}' | submit_text: '{form_submit}' }}}}
+
+"""
+ elif page_type == "category":
+ content_section = f"""
+
+ {headline}
+ {tagline}
+
+
+ {main_content}
+
+
+"""
+ else:
+ content_section = f"""
+
+ {headline}
+ {f'{tagline}
' if tagline else ''}
+
+ {main_content}
+
+
+"""
+
+ # Validate no forbidden tags
+ is_valid, errors = validate_liquid_content(content_section)
+ if not is_valid:
+ return {
+ "error": "Generated content contains forbidden Liquid tags",
+ "forbidden_tags_found": errors,
+ "fix": "Remove forbidden tags and use verified alternatives",
+ }
+
+ # Build complete HTML
+ html = f"""
+
+
+
+
+ {site_title}
+
+
+
+
+
+
+
+{content_section}
+
+
+
+
+
+
+"""
+
+ return {
+ "html": html,
+ "page_type": page_type,
+ "slots_filled": list(slot_values.keys()),
+ "style": "90s_nostalgia",
+ "validated": True,
+ "next_step": "Create page using create_page tool, then verify with verify_page",
+ }
+
+
+from deepagents import SubAgent
+
+template_designer_subagent: SubAgent = {
+ "name": "template-designer-subagent",
+ "description": """Use this subagent for blog/CMS page creation with verified templates:
+- Selecting appropriate page templates (home, category, write, about)
+- Getting correct Liquid tag syntax (NEVER hallucinate tags)
+- Generating styled HTML with 90s nostalgia CSS
+- Verifying pages render correctly after creation
+
+CRITICAL: Only uses VERIFIED Liquid tags. Never uses render_api_form (doesn't exist).""",
+ "system_prompt": TEMPLATE_SUBAGENT_PROMPT,
+ "tools": [
+ list_templates,
+ select_template,
+ get_liquid_tag,
+ generate_styled_page,
+ verify_page,
+ ],
+}
+
+
+# Export for use in main agent
+__all__ = [
+ "template_designer_subagent",
+ "list_templates",
+ "select_template",
+ "get_liquid_tag",
+ "generate_styled_page",
+ "verify_page",
+ "VERIFIED_LIQUID_TAGS",
+ "FORBIDDEN_TAGS",
+ "BLOG_TEMPLATES",
+]
diff --git a/violet-app-agent/apps/agent/src/violet_app_agent/templates/styles/90s_nostalgia.css b/violet-app-agent/apps/agent/src/violet_app_agent/templates/styles/90s_nostalgia.css
new file mode 100644
index 000000000..8ca204fce
--- /dev/null
+++ b/violet-app-agent/apps/agent/src/violet_app_agent/templates/styles/90s_nostalgia.css
@@ -0,0 +1,478 @@
+/*
+ * 90s Low-Tech Nostalgia CSS
+ *
+ * Design Philosophy: "Don't Make Me Think Revisited"
+ * - Clear, obvious navigation
+ * - Every click should be obvious
+ * - Room to breathe
+ * - Satisfice: Users scan, not read
+ */
+
+:root {
+ /* Colors: Muted, warm, nostalgic */
+ --bg-cream: #f5f0e6;
+ --bg-paper: #fffef9;
+ --text-ink: #2c2c2c;
+ --text-muted: #5a5a5a;
+ --accent-teal: #2a9d8f;
+ --accent-coral: #e76f51;
+ --border-soft: #d4cfc4;
+
+ /* Typography: Georgia for that web 1.0 feel */
+ --font-serif: Georgia, "Times New Roman", serif;
+ --font-mono: "Courier New", Courier, monospace;
+
+ /* Sizes: Generous, readable */
+ --text-sm: 0.875rem;
+ --text-base: 1.125rem;
+ --text-lg: 1.5rem;
+ --text-xl: 2rem;
+ --text-2xl: 2.5rem;
+
+ /* Spacing: Room to breathe */
+ --space-xs: 0.5rem;
+ --space-sm: 1rem;
+ --space-md: 2rem;
+ --space-lg: 4rem;
+ --space-xl: 8rem;
+}
+
+/* Reset */
+*, *::before, *::after {
+ box-sizing: border-box;
+}
+
+/* Base: Paper-like background */
+body {
+ font-family: var(--font-serif);
+ font-size: var(--text-base);
+ line-height: 1.7;
+ color: var(--text-ink);
+ background: var(--bg-cream);
+ margin: 0;
+ padding: 0;
+ min-height: 100vh;
+}
+
+/* CSS Grid Layout: Simple content-width container */
+.container {
+ display: grid;
+ grid-template-columns:
+ [full-start] minmax(var(--space-md), 1fr)
+ [content-start] minmax(0, 720px)
+ [content-end] minmax(var(--space-md), 1fr)
+ [full-end];
+ row-gap: var(--space-lg);
+ padding-top: var(--space-md);
+ padding-bottom: var(--space-xl);
+}
+
+.container > * {
+ grid-column: content;
+}
+
+/* Full-bleed elements */
+.full-bleed {
+ grid-column: full;
+}
+
+/* Typography: Semantic, scannable */
+h1, h2, h3 {
+ font-family: var(--font-serif);
+ font-weight: normal;
+ letter-spacing: -0.02em;
+ margin: 0 0 var(--space-sm);
+}
+
+h1 {
+ font-size: var(--text-2xl);
+ line-height: 1.2;
+}
+
+h2 {
+ font-size: var(--text-xl);
+ color: var(--text-muted);
+}
+
+h3 {
+ font-size: var(--text-lg);
+}
+
+p {
+ margin: 0 0 var(--space-sm);
+}
+
+/* Links: Underlined, obvious */
+a {
+ color: var(--accent-teal);
+ text-decoration: underline;
+ text-underline-offset: 3px;
+ transition: color 0.2s ease;
+}
+
+a:hover {
+ color: var(--accent-coral);
+}
+
+/* Cards: Subtle elevation */
+.card {
+ background: var(--bg-paper);
+ border: 1px solid var(--border-soft);
+ padding: var(--space-md);
+ margin-bottom: var(--space-md);
+}
+
+.card h3 {
+ margin-top: 0;
+}
+
+/* Blockquotes: Pull quotes */
+blockquote {
+ font-style: italic;
+ border-left: 3px solid var(--accent-teal);
+ padding-left: var(--space-md);
+ margin: var(--space-md) 0;
+ color: var(--text-muted);
+}
+
+blockquote cite {
+ display: block;
+ margin-top: var(--space-sm);
+ font-style: normal;
+ font-size: var(--text-sm);
+}
+
+/* Navigation: Simple, horizontal */
+nav {
+ display: flex;
+ gap: var(--space-md);
+ padding: var(--space-md) 0;
+ border-bottom: 1px solid var(--border-soft);
+}
+
+nav a {
+ text-decoration: none;
+ color: var(--text-ink);
+}
+
+nav a:hover {
+ text-decoration: underline;
+}
+
+nav a.active {
+ color: var(--accent-teal);
+}
+
+/* Header */
+header {
+ margin-bottom: var(--space-md);
+}
+
+/* Hero section */
+.hero {
+ text-align: center;
+ padding: var(--space-lg) 0;
+}
+
+.hero h1 {
+ margin-bottom: var(--space-sm);
+}
+
+.hero p {
+ color: var(--text-muted);
+ font-size: var(--text-lg);
+}
+
+/* CTA buttons in hero */
+.hero-cta {
+ display: flex;
+ gap: var(--space-sm);
+ justify-content: center;
+ margin-top: var(--space-md);
+}
+
+.hero-cta a {
+ padding: var(--space-sm) var(--space-md);
+ text-decoration: none;
+ border: 1px solid var(--border-soft);
+ background: var(--bg-paper);
+}
+
+.hero-cta a:hover {
+ background: var(--accent-teal);
+ color: white;
+ border-color: var(--accent-teal);
+}
+
+/* Forms: Clean, accessible */
+.form-group {
+ margin-bottom: var(--space-md);
+}
+
+label {
+ display: block;
+ margin-bottom: var(--space-xs);
+ font-weight: bold;
+}
+
+input, textarea, select {
+ width: 100%;
+ padding: var(--space-sm);
+ font-family: var(--font-serif);
+ font-size: var(--text-base);
+ border: 1px solid var(--border-soft);
+ background: var(--bg-paper);
+ border-radius: 0;
+}
+
+input:focus, textarea:focus, select:focus {
+ outline: 2px solid var(--accent-teal);
+ outline-offset: 2px;
+}
+
+textarea {
+ min-height: 200px;
+ resize: vertical;
+}
+
+button, .btn {
+ font-family: var(--font-serif);
+ font-size: var(--text-base);
+ padding: var(--space-sm) var(--space-md);
+ background: var(--accent-teal);
+ color: white;
+ border: none;
+ cursor: pointer;
+ text-decoration: none;
+ display: inline-block;
+}
+
+button:hover, .btn:hover {
+ background: var(--accent-coral);
+}
+
+/* Post cards grid */
+.posts-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: var(--space-md);
+}
+
+.post-card {
+ background: var(--bg-paper);
+ border: 1px solid var(--border-soft);
+ padding: var(--space-md);
+}
+
+.post-card h3 {
+ margin-top: 0;
+ margin-bottom: var(--space-xs);
+}
+
+.post-card h3 a {
+ text-decoration: none;
+ color: var(--text-ink);
+}
+
+.post-card h3 a:hover {
+ color: var(--accent-teal);
+}
+
+.post-card .meta {
+ font-size: var(--text-sm);
+ color: var(--text-muted);
+ margin-bottom: var(--space-sm);
+}
+
+.post-card .excerpt {
+ color: var(--text-muted);
+}
+
+/* Category pills */
+.category-pill {
+ display: inline-block;
+ font-size: var(--text-sm);
+ padding: var(--space-xs) var(--space-sm);
+ background: var(--bg-cream);
+ border: 1px solid var(--border-soft);
+ color: var(--text-muted);
+ text-decoration: none;
+ margin-right: var(--space-xs);
+ margin-bottom: var(--space-xs);
+}
+
+.category-pill:hover {
+ background: var(--accent-teal);
+ color: white;
+ border-color: var(--accent-teal);
+}
+
+/* Section spacing */
+section {
+ margin-bottom: var(--space-lg);
+}
+
+section h2 {
+ margin-bottom: var(--space-md);
+}
+
+/* Content cards (categories on home) */
+.content-cards {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: var(--space-md);
+}
+
+.content-card {
+ background: var(--bg-paper);
+ border: 1px solid var(--border-soft);
+ padding: var(--space-md);
+ text-align: center;
+}
+
+.content-card h3 {
+ margin-top: 0;
+}
+
+.content-card p {
+ color: var(--text-muted);
+ margin-bottom: var(--space-md);
+}
+
+/* Footer */
+footer {
+ margin-top: var(--space-xl);
+ padding: var(--space-md) 0;
+ border-top: 1px solid var(--border-soft);
+ font-size: var(--text-sm);
+ color: var(--text-muted);
+ text-align: center;
+}
+
+/* Article/Post styling */
+article {
+ max-width: 100%;
+}
+
+article header {
+ margin-bottom: var(--space-lg);
+ text-align: center;
+}
+
+article header h1 {
+ margin-bottom: var(--space-sm);
+}
+
+article header .meta {
+ color: var(--text-muted);
+ font-size: var(--text-sm);
+}
+
+article .content {
+ line-height: 1.8;
+}
+
+article .content p {
+ margin-bottom: var(--space-md);
+}
+
+article .content img {
+ max-width: 100%;
+ height: auto;
+ margin: var(--space-md) 0;
+}
+
+/* About section styling */
+.about {
+ background: var(--bg-paper);
+ border: 1px solid var(--border-soft);
+ padding: var(--space-md);
+ margin: var(--space-md) 0;
+}
+
+.about h3 {
+ margin-top: 0;
+}
+
+/* Timeline/Journey styling */
+.timeline {
+ border-left: 2px solid var(--border-soft);
+ padding-left: var(--space-md);
+ margin: var(--space-md) 0;
+}
+
+.timeline-item {
+ margin-bottom: var(--space-md);
+ position: relative;
+}
+
+.timeline-item::before {
+ content: "";
+ position: absolute;
+ left: calc(-1 * var(--space-md) - 5px);
+ top: 8px;
+ width: 10px;
+ height: 10px;
+ background: var(--accent-teal);
+ border-radius: 50%;
+}
+
+.timeline-item h3 {
+ margin-top: 0;
+ margin-bottom: var(--space-xs);
+}
+
+/* Empty state */
+.empty-state {
+ text-align: center;
+ padding: var(--space-xl) var(--space-md);
+ color: var(--text-muted);
+}
+
+.empty-state p {
+ margin-bottom: var(--space-md);
+}
+
+/* Responsive: Mobile-first */
+@media (max-width: 600px) {
+ :root {
+ --text-base: 1rem;
+ --text-lg: 1.25rem;
+ --text-xl: 1.5rem;
+ --text-2xl: 2rem;
+ --space-md: 1.5rem;
+ --space-lg: 2rem;
+ --space-xl: 4rem;
+ }
+
+ nav {
+ flex-direction: column;
+ gap: var(--space-sm);
+ }
+
+ .hero-cta {
+ flex-direction: column;
+ }
+
+ .posts-grid,
+ .content-cards {
+ grid-template-columns: 1fr;
+ }
+}
+
+/* Print styles */
+@media print {
+ body {
+ background: white;
+ color: black;
+ }
+
+ nav, footer, .hero-cta {
+ display: none;
+ }
+
+ a {
+ color: black;
+ text-decoration: underline;
+ }
+}
diff --git a/violet-app-agent/docs/RFC-002-template-ecosystem.md b/violet-app-agent/docs/RFC-002-template-ecosystem.md
new file mode 100644
index 000000000..1aedc629b
--- /dev/null
+++ b/violet-app-agent/docs/RFC-002-template-ecosystem.md
@@ -0,0 +1,892 @@
+# RFC-002: Template Ecosystem & Agent Actions Critique
+
+**Status:** Draft
+**Author:** Violet App Agent Team
+**Created:** 2025-12-09
+**Related PR:** #1719
+**Depends On:** RFC-001 (Plan Mode UX)
+
+## Executive Summary
+
+This RFC critiques the agent's actions during the "Conscious Observer" blog build, defines success criteria for verification, and proposes a Template Ecosystem that ensures deterministic, working page generation with 90's low-tech nostalgia styling.
+
+---
+
+## Part 1: Composite Evaluation of Agent Journey
+
+### What the Agent Built
+
+| Page | URL | Status | Quality |
+|------|-----|--------|---------|
+| Landing | `/` | Default | Generic Violet Rails welcome |
+| Home | `/home` | Working | Good structure, 3 content cards |
+| Toronto | `/toronto` | Working | Rich narrative, blockquote |
+| Jersey City | `/jersey-city` | Working | Most personalized, references chaiwithjai.com |
+| Stories | `/stories` | Empty | Blank page - no content |
+| Write | `/write` | **BROKEN** | `NoMethodError: undefined method 'render_api_form'` |
+
+### Screenshot Evidence
+
+```
+eval-01-landing-page.png → Generic "Hello from conscious-observer"
+eval-02-home-page.png → 3 cards: Toronto, Jersey City, Observation
+eval-03-stories-empty.png → Completely blank white page
+eval-04-toronto-page.png → Rich content with cultural narrative
+eval-05-jersey-city-page.png → Jai's story, chai references, November 2025
+eval-06-write-error.png → NoMethodError stack trace
+```
+
+### What Went Right
+
+1. **Subdomain Creation**: `conscious-observer` created successfully
+2. **Content Generation**: Agent generated compelling, personalized content:
+ - Toronto: "cultural crossroads", "international origins"
+ - Jersey City: "November 2025", "Heights", references to chaiwithjai.com
+3. **Information Architecture**: Logical page hierarchy with clear navigation
+4. **Personal Voice**: Content matches the "Jai Bhagat" persona well
+
+### What Went Wrong
+
+1. **Non-Existent Liquid Tag**: Agent used `render_api_form` which doesn't exist in Violet Rails
+ - Should have used `render_form` or built a static HTML form
+ - This is a **domain knowledge gap** - agent doesn't know available Liquid tags
+
+2. **Empty Stories Page**: `/stories` was created but has no content
+ - Either the page content failed to save, or agent didn't populate it
+
+3. **No Verification**: Agent didn't verify pages actually render before reporting success
+ - A simple GET request to each page would have caught the `/write` error
+
+4. **No Styling**: Pages are unstyled browser defaults
+ - No CSS was applied
+ - Functional but not visually appealing
+
+---
+
+## Part 2: Success Criteria for Verification
+
+### Definition of "Done" for Page Creation
+
+A page is only successfully created when ALL criteria pass:
+
+```python
+class PageVerificationCriteria:
+ """Success criteria for page creation verification."""
+
+ MUST_PASS = {
+ "page_exists": "Page record exists in database",
+ "page_renders": "GET request returns 200 (not 500)",
+ "no_errors": "No NoMethodError, NameError, or SyntaxError",
+ "content_present": "Page has visible content (not blank)",
+ "links_work": "Internal links resolve to existing pages",
+ }
+
+ SHOULD_PASS = {
+ "styling_applied": "CSS styles are present",
+ "semantic_html": "Uses proper heading hierarchy",
+ "mobile_friendly": "Viewport meta tag present",
+ }
+
+ NICE_TO_HAVE = {
+ "accessibility": "Alt text on images, proper ARIA",
+ "performance": "Page loads in < 3 seconds",
+ }
+```
+
+### Verification Tool Implementation
+
+```python
+@tool
+def verify_page(subdomain: str, path: str) -> dict:
+ """
+ Verify a page was created successfully and renders without errors.
+
+ Args:
+ subdomain: The subdomain name (e.g., 'conscious-observer')
+ path: The page path (e.g., '/home')
+
+ Returns:
+ dict with verification results
+ """
+ url = f"http://{subdomain}.localhost:5250{path}"
+
+ try:
+ response = requests.get(url, timeout=10)
+
+ return {
+ "success": response.status_code == 200,
+ "status_code": response.status_code,
+ "has_content": len(response.text) > 100,
+ "has_error": "NoMethodError" in response.text or "Error" in response.text,
+ "content_length": len(response.text),
+ "url": url,
+ }
+ except Exception as e:
+ return {
+ "success": False,
+ "error": str(e),
+ "url": url,
+ }
+```
+
+### E2E Verification Test Suite
+
+```python
+class TestBlogDelivery:
+ """E2E tests verifying the delivered blog artifact."""
+
+ @pytest.fixture
+ def subdomain(self):
+ return "conscious-observer"
+
+ def test_home_page_renders(self, subdomain):
+ """Home page should render with content cards."""
+ result = verify_page(subdomain, "/home")
+ assert result["success"], f"Home page failed: {result}"
+ assert result["has_content"], "Home page is empty"
+ assert not result["has_error"], "Home page has errors"
+
+ def test_all_linked_pages_work(self, subdomain):
+ """All links from home should resolve."""
+ links = ["/stories", "/write", "/toronto", "/jersey-city"]
+ failures = []
+
+ for link in links:
+ result = verify_page(subdomain, link)
+ if not result["success"] or result["has_error"]:
+ failures.append(f"{link}: {result}")
+
+ assert not failures, f"Broken links: {failures}"
+
+ def test_no_500_errors(self, subdomain):
+ """No page should return a 500 error."""
+ pages = ["/", "/home", "/stories", "/write", "/toronto", "/jersey-city"]
+
+ for page in pages:
+ result = verify_page(subdomain, page)
+ assert result.get("status_code") != 500, f"{page} returns 500"
+```
+
+---
+
+## Part 3: Real-World Expectations
+
+### What Users Expect vs What They Get
+
+| Expectation | Current Reality | Gap |
+|-------------|-----------------|-----|
+| "A working blog" | 4/6 pages work | `/stories` empty, `/write` broken |
+| "Professional styling" | Browser defaults | No CSS applied |
+| "Ready to add content" | Missing forms | Can't submit stories |
+| "Mobile friendly" | Unstyled = responsive-ish | No viewport meta |
+
+### User Journey Pain Points
+
+1. **First Impression**: Landing at `/` shows generic welcome, not custom blog
+2. **Broken Flow**: Clicking "Share Your Story" leads to error page
+3. **Empty Sections**: Stories page is blank - looks like something broke
+4. **No Visual Identity**: Plain HTML doesn't convey blog personality
+
+### Real-World Success Definition
+
+```yaml
+definition_of_done:
+ functional:
+ - All 6 pages render without errors
+ - Navigation links all work
+ - Story submission form functional
+ - Content displays correctly
+
+ visual:
+ - Consistent styling across pages
+ - Typography reflects blog personality
+ - Color scheme applied
+ - Mobile responsive
+
+ content:
+ - Home has introductory content
+ - Category pages have descriptions
+ - At least one sample post
+ - Clear calls-to-action
+```
+
+---
+
+## Part 4: Deterministic Tool Calls
+
+### The Problem
+
+Agent generated Liquid code using non-existent helpers:
+
+```liquid
+
+{{ render_api_form | namespace: "story" | submit_text: "Share Your Story" }}
+
+
+{{ cms:snippet story_form }}
+
+```
+
+### Root Cause
+
+The agent:
+1. Knows Violet Rails has forms
+2. Doesn't know the exact Liquid tag syntax
+3. "Hallucinated" a plausible-sounding tag name
+4. Didn't verify the tag exists before using it
+
+### Solution: Deterministic Template Library
+
+Instead of generating Liquid code, agent selects from verified templates:
+
+```python
+VERIFIED_LIQUID_TAGS = {
+ # Content tags (SAFE - always work)
+ "text": "{{ cms:text identifier }}",
+ "markdown": "{{ cms:markdown identifier }}",
+ "wysiwyg": "{{ cms:wysiwyg identifier }}",
+
+ # Asset tags (SAFE)
+ "asset": "{{ cms:asset identifier, as: image }}",
+ "file": "{{ cms:file identifier }}",
+
+ # Snippet tags (SAFE - if snippet exists)
+ "snippet": "{{ cms:snippet identifier }}",
+
+ # Collection tags (SAFE)
+ "collection": "{{ cms:collection identifier | namespace: 'name' }}",
+
+ # Form tags (USE render_form NOT render_api_form)
+ "form": "{{ render_form | namespace: 'name' }}",
+}
+
+FORBIDDEN_TAGS = {
+ "render_api_form": "Does not exist - use render_form",
+ "api_resource": "Does not exist",
+}
+```
+
+### Validation Before Execution
+
+```python
+def validate_liquid_content(content: str) -> tuple[bool, list[str]]:
+ """
+ Validate Liquid content only uses known tags.
+
+ Returns:
+ (is_valid, list of errors)
+ """
+ errors = []
+
+ for forbidden, reason in FORBIDDEN_TAGS.items():
+ if forbidden in content:
+ errors.append(f"Forbidden tag '{forbidden}': {reason}")
+
+ return len(errors) == 0, errors
+```
+
+---
+
+## Part 5: Template Ecosystem for Blog Use Case
+
+### Design Philosophy: "Don't Make Me Think Revisited"
+
+Following Steve Krug's principles:
+
+1. **Don't make users think** - Clear, obvious navigation
+2. **Every click should be obvious** - No mystery meat navigation
+3. **Get rid of half the words, then get rid of half of what's left**
+4. **Satisfice** - Users don't read, they scan
+
+### Template Structure
+
+```
+templates/
+├── blog/
+│ ├── base.liquid # Base layout with CSS Grid
+│ ├── home.liquid # Featured posts + categories
+│ ├── post_index.liquid # All posts with filters
+│ ├── post_show.liquid # Single post view
+│ ├── category.liquid # Posts by category
+│ ├── author.liquid # Author profile + posts
+│ └── write.liquid # Story submission form
+├── components/
+│ ├── header.liquid # Navigation
+│ ├── footer.liquid # Footer with links
+│ ├── post_card.liquid # Post preview card
+│ └── category_pill.liquid # Category badge
+└── styles/
+ └── 90s-nostalgia.css # The aesthetic
+```
+
+### 90's Low-Tech Nostalgia CSS
+
+```css
+/* 90s-nostalgia.css - Design that breathes */
+
+:root {
+ /* Colors: Muted, warm, nostalgic */
+ --bg-cream: #f5f0e6;
+ --bg-paper: #fffef9;
+ --text-ink: #2c2c2c;
+ --text-muted: #5a5a5a;
+ --accent-teal: #2a9d8f;
+ --accent-coral: #e76f51;
+ --border-soft: #d4cfc4;
+
+ /* Typography: Georgia for that web 1.0 feel */
+ --font-serif: Georgia, "Times New Roman", serif;
+ --font-mono: "Courier New", Courier, monospace;
+
+ /* Sizes: Generous, readable */
+ --text-sm: 0.875rem;
+ --text-base: 1.125rem;
+ --text-lg: 1.5rem;
+ --text-xl: 2rem;
+ --text-2xl: 2.5rem;
+
+ /* Spacing: Room to breathe */
+ --space-xs: 0.5rem;
+ --space-sm: 1rem;
+ --space-md: 2rem;
+ --space-lg: 4rem;
+ --space-xl: 8rem;
+}
+
+/* Base: Paper-like background */
+body {
+ font-family: var(--font-serif);
+ font-size: var(--text-base);
+ line-height: 1.7;
+ color: var(--text-ink);
+ background: var(--bg-cream);
+ margin: 0;
+ padding: 0;
+}
+
+/* CSS Grid Layout: Simple 12-column */
+.container {
+ display: grid;
+ grid-template-columns:
+ [full-start] minmax(var(--space-md), 1fr)
+ [content-start] minmax(0, 720px)
+ [content-end] minmax(var(--space-md), 1fr)
+ [full-end];
+ row-gap: var(--space-lg);
+}
+
+.container > * {
+ grid-column: content;
+}
+
+/* Full-bleed elements */
+.full-bleed {
+ grid-column: full;
+}
+
+/* Typography: Semantic, scannable */
+h1, h2, h3 {
+ font-family: var(--font-serif);
+ font-weight: normal;
+ letter-spacing: -0.02em;
+ margin: 0 0 var(--space-sm);
+}
+
+h1 {
+ font-size: var(--text-2xl);
+ line-height: 1.2;
+}
+
+h2 {
+ font-size: var(--text-xl);
+ color: var(--text-muted);
+}
+
+h3 {
+ font-size: var(--text-lg);
+}
+
+p {
+ margin: 0 0 var(--space-sm);
+}
+
+/* Links: Underlined, obvious */
+a {
+ color: var(--accent-teal);
+ text-decoration: underline;
+ text-underline-offset: 3px;
+}
+
+a:hover {
+ color: var(--accent-coral);
+}
+
+/* Cards: Subtle elevation */
+.card {
+ background: var(--bg-paper);
+ border: 1px solid var(--border-soft);
+ padding: var(--space-md);
+ margin-bottom: var(--space-md);
+}
+
+/* Blockquotes: Pull quotes */
+blockquote {
+ font-style: italic;
+ border-left: 3px solid var(--accent-teal);
+ padding-left: var(--space-md);
+ margin: var(--space-md) 0;
+ color: var(--text-muted);
+}
+
+/* Navigation: Simple, horizontal */
+nav {
+ display: flex;
+ gap: var(--space-md);
+ padding: var(--space-md) 0;
+ border-bottom: 1px solid var(--border-soft);
+}
+
+nav a {
+ text-decoration: none;
+ color: var(--text-ink);
+}
+
+nav a:hover {
+ text-decoration: underline;
+}
+
+/* Forms: Clean, accessible */
+.form-group {
+ margin-bottom: var(--space-md);
+}
+
+label {
+ display: block;
+ margin-bottom: var(--space-xs);
+ font-weight: bold;
+}
+
+input, textarea {
+ width: 100%;
+ padding: var(--space-sm);
+ font-family: var(--font-serif);
+ font-size: var(--text-base);
+ border: 1px solid var(--border-soft);
+ background: var(--bg-paper);
+}
+
+textarea {
+ min-height: 200px;
+ resize: vertical;
+}
+
+button {
+ font-family: var(--font-serif);
+ font-size: var(--text-base);
+ padding: var(--space-sm) var(--space-md);
+ background: var(--accent-teal);
+ color: white;
+ border: none;
+ cursor: pointer;
+}
+
+button:hover {
+ background: var(--accent-coral);
+}
+
+/* Post cards grid */
+.posts-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: var(--space-md);
+}
+
+/* Category pills */
+.category-pill {
+ display: inline-block;
+ font-size: var(--text-sm);
+ padding: var(--space-xs) var(--space-sm);
+ background: var(--bg-cream);
+ border: 1px solid var(--border-soft);
+ color: var(--text-muted);
+ text-decoration: none;
+}
+
+/* Footer */
+footer {
+ margin-top: var(--space-xl);
+ padding: var(--space-md) 0;
+ border-top: 1px solid var(--border-soft);
+ font-size: var(--text-sm);
+ color: var(--text-muted);
+}
+
+/* Responsive: Mobile-first */
+@media (max-width: 600px) {
+ :root {
+ --text-base: 1rem;
+ --text-xl: 1.5rem;
+ --text-2xl: 2rem;
+ --space-md: 1.5rem;
+ --space-lg: 2rem;
+ }
+
+ nav {
+ flex-direction: column;
+ gap: var(--space-sm);
+ }
+}
+```
+
+### Template: Home Page
+
+```liquid
+
+
+
+
+
+
+ {{ cms:text site_title }}
+
+
+
+
+
+
+
+
+ {{ cms:text headline }}
+ {{ cms:text tagline }}
+
+
+
+ Explore
+
+ {{ cms:snippet category_cards }}
+
+
+
+
+ Featured Stories
+ {{ cms:collection featured_posts | namespace: 'posts' }}
+
+
+
+
+
+
+
+```
+
+### Template: Write/Submit Page (FIXED)
+
+```liquid
+
+
+
+
+
+
+
+ Share Your Story | {{ cms:text site_title }}
+
+
+
+
+
+
+
+ Share Your Story
+ {{ cms:text write_intro }}
+
+
+ {{ render_form | namespace: 'stories' | submit_text: 'Share Your Story' }}
+
+
+
+
+
+
+```
+
+---
+
+## Part 6: Template Subagent Architecture
+
+### Deep Agent Integration
+
+Following the DIAGNOSE → MULTI-EXPERT → ARTIFACT-FIRST → DOMAIN-NATIVE pattern:
+
+```
+ ┌─────────────────────┐
+ │ Main Agent │
+ │ (Orchestrator) │
+ └─────────┬───────────┘
+ │
+ ┌───────────────────────┼───────────────────────┐
+ │ │ │
+ ▼ ▼ ▼
+ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
+ │ Architect │ │ Template │ │ Deployer │
+ │ Subagent │ │ Subagent │◄──NEW │ Subagent │
+ └──────────────┘ └──────────────┘ └──────────────┘
+ │ │ │
+ │ │ │
+ ┌──────┴──────┐ ┌──────┴──────┐ ┌──────┴──────┐
+ │ Data Model │ │ Template │ │ GitHub │
+ │ Design │ │ Selection │ │ Deployment │
+ └─────────────┘ │ + Styling │ └─────────────┘
+ │ + Verify │
+ └─────────────┘
+```
+
+### Template Subagent Definition
+
+```python
+# src/violet_app_agent/subagents/template_designer.py
+
+from langchain_core.tools import tool
+from langgraph.prebuilt import create_react_agent
+from langchain_anthropic import ChatAnthropic
+
+TEMPLATE_SUBAGENT_PROMPT = """You are the Template Designer subagent for the Violet App Agent.
+
+Your role is to:
+1. SELECT appropriate templates from the verified template library
+2. CUSTOMIZE templates with user-specific content
+3. APPLY styling (90s nostalgia CSS by default)
+4. VERIFY each page renders successfully after creation
+
+CRITICAL RULES:
+- ONLY use Liquid tags from VERIFIED_LIQUID_TAGS
+- NEVER use render_api_form (it doesn't exist) - use render_form
+- ALWAYS verify pages after creation
+- Report any rendering errors immediately
+
+Available Templates:
+- blog/home.liquid - Homepage with hero, categories, featured posts
+- blog/post_index.liquid - All posts listing
+- blog/post_show.liquid - Single post view
+- blog/category.liquid - Posts filtered by category
+- blog/write.liquid - Story submission form (uses render_form)
+
+Default Style: 90s-nostalgia.css
+- Cream background, paper-white cards
+- Georgia serif typography
+- Teal/coral accents
+- CSS Grid layout
+- Generous whitespace
+"""
+
+@tool
+def select_template(page_type: str) -> dict:
+ """
+ Select the appropriate template for a page type.
+
+ Args:
+ page_type: One of 'home', 'post_index', 'post_show', 'category', 'write'
+
+ Returns:
+ Template definition with content slots
+ """
+ templates = {
+ "home": {
+ "file": "blog/home.liquid",
+ "slots": ["headline", "tagline", "category_cards", "featured_posts"],
+ "style": "90s-nostalgia.css",
+ },
+ "write": {
+ "file": "blog/write.liquid",
+ "slots": ["write_intro"],
+ "form": "render_form", # NOT render_api_form!
+ "style": "90s-nostalgia.css",
+ },
+ # ... other templates
+ }
+ return templates.get(page_type, {})
+
+
+@tool
+def apply_template(
+ subdomain: str,
+ path: str,
+ template_name: str,
+ content: dict
+) -> dict:
+ """
+ Apply a template to create a page with styling.
+
+ Args:
+ subdomain: Target subdomain
+ path: Page path (e.g., '/home')
+ template_name: Template to use (e.g., 'blog/home.liquid')
+ content: Dict of slot names to content values
+
+ Returns:
+ Result with page URL and verification status
+ """
+ # Load template
+ # Fill slots
+ # Apply CSS
+ # Create page via rails_runner
+ # VERIFY the page renders
+ verification = verify_page(subdomain, path)
+
+ return {
+ "page_url": f"http://{subdomain}.localhost:5250{path}",
+ "template": template_name,
+ "verified": verification["success"],
+ "errors": verification.get("errors", []),
+ }
+
+
+@tool
+def verify_page(subdomain: str, path: str) -> dict:
+ """Verify a page renders without errors."""
+ # Implementation from Part 2
+ pass
+
+
+def create_template_subagent():
+ """Create the Template Designer subagent."""
+ model = ChatAnthropic(model="claude-sonnet-4-20250514", temperature=0)
+
+ tools = [
+ select_template,
+ apply_template,
+ verify_page,
+ ]
+
+ return create_react_agent(
+ model,
+ tools,
+ state_modifier=TEMPLATE_SUBAGENT_PROMPT,
+ )
+```
+
+### Integration with Main Agent
+
+```python
+# In agent.py - Updated tool list
+
+def create_agent():
+ """Create the main Violet App Agent with template subagent."""
+
+ # Subagents
+ architect = create_architect_subagent()
+ template_designer = create_template_subagent() # NEW
+ deployer = create_deployer_subagent()
+
+ tools = [
+ # Infrastructure
+ create_subdomain,
+ create_namespace,
+
+ # Templates (NEW - deterministic)
+ select_template,
+ apply_template,
+ verify_page,
+
+ # Deployment
+ trigger_deployment,
+ ]
+
+ return create_react_agent(model, tools, state_modifier=SYSTEM_PROMPT)
+```
+
+---
+
+## Part 7: Implementation Roadmap
+
+### Phase 1: Fix Critical Issues (Immediate)
+
+1. [ ] Fix `/write` page - replace `render_api_form` with `render_form`
+2. [ ] Add content to `/stories` page
+3. [ ] Implement `verify_page` tool
+4. [ ] Add verification step after each `create_page` call
+
+### Phase 2: Template Ecosystem (Week 1)
+
+1. [ ] Create verified template library (6 templates)
+2. [ ] Implement `select_template` tool
+3. [ ] Implement `apply_template` tool with CSS injection
+4. [ ] Write 90s-nostalgia.css
+
+### Phase 3: Template Subagent (Week 2)
+
+1. [ ] Create Template Designer subagent
+2. [ ] Integrate with main agent graph
+3. [ ] Add template selection to Plan Phase
+4. [ ] E2E tests for template application
+
+### Phase 4: Verification Suite (Week 3)
+
+1. [ ] Full verification test suite
+2. [ ] Automated screenshot comparison
+3. [ ] Mobile responsiveness checks
+4. [ ] Performance baseline tests
+
+---
+
+## Appendix: Verified Liquid Tags Reference
+
+```yaml
+# SAFE TO USE - These tags exist and work
+content_tags:
+ - "{{ cms:text identifier }}"
+ - "{{ cms:markdown identifier }}"
+ - "{{ cms:wysiwyg identifier }}"
+
+asset_tags:
+ - "{{ cms:asset identifier, as: image }}"
+ - "{{ cms:file identifier }}"
+
+snippet_tags:
+ - "{{ cms:snippet identifier }}"
+
+collection_tags:
+ - "{{ cms:collection identifier | namespace: 'name' }}"
+
+form_tags:
+ - "{{ render_form | namespace: 'name' }}" # CORRECT
+
+# FORBIDDEN - These DO NOT exist
+forbidden:
+ - render_api_form # Use render_form instead
+ - api_resource # Does not exist
+ - render_api # Does not exist
+```
+
+---
+
+## Conclusion
+
+The Conscious Observer blog build revealed critical gaps in the agent's domain knowledge and verification processes. By implementing:
+
+1. **Deterministic templates** with verified Liquid tags
+2. **Mandatory verification** after page creation
+3. **A Template Subagent** following Deep Agent architecture
+4. **90s nostalgia styling** for visual identity
+
+We can ensure that future builds produce working, styled, verified artifacts that match user expectations from Day 0.
+
+The template ecosystem transforms the agent from a "generate and hope" model to a "select, apply, and verify" model - dramatically improving reliability while maintaining flexibility for customization.
diff --git a/violet-app-agent/docs/TEMPLATE_ECOSYSTEM_CONTRIBUTING.md b/violet-app-agent/docs/TEMPLATE_ECOSYSTEM_CONTRIBUTING.md
new file mode 100644
index 000000000..53504f993
--- /dev/null
+++ b/violet-app-agent/docs/TEMPLATE_ECOSYSTEM_CONTRIBUTING.md
@@ -0,0 +1,588 @@
+# Template Ecosystem Contributing Guide
+
+> Build AI agents that deploy Rails apps from natural language.
+
+This guide helps hackathon participants understand, extend, and contribute to the Template Ecosystem feature in the Violet App Agent.
+
+## Quick Links
+
+| Resource | Description |
+|----------|-------------|
+| [Demo Landing Page](../apps/agent/demo/index.html) | Try the agent live |
+| [RFC-002](./RFC-002-template-ecosystem.md) | Design philosophy and architecture |
+| [Template Designer](../apps/agent/src/violet_app_agent/subagents/template_designer.py) | Core implementation |
+| [90s Nostalgia CSS](../apps/agent/src/violet_app_agent/templates/styles/90s_nostalgia.css) | Design system |
+
+---
+
+## What is the Template Ecosystem?
+
+The Template Ecosystem solves a critical problem: **AI agents hallucinate non-existent code**.
+
+When we asked LLMs to generate Liquid templates for Violet Rails, they invented tags like `render_api_form` that don't exist, causing runtime errors.
+
+**Our solution:** A deterministic template system with:
+1. **Verified Liquid Tags** - Whitelist of tags that actually exist
+2. **90s Nostalgia CSS** - Automatic styling baked into every page
+3. **Page Verification** - HTTP checks confirm pages render without errors
+
+### The Workflow
+
+```
+1. generate_styled_page(page_type, slot_values, nav_links)
+ → Returns styled HTML with 90s nostalgia CSS
+
+2. create_page(subdomain, title, slug, content=styled_html)
+ → Creates the page in CMS
+
+3. verify_page(subdomain, slug)
+ → Confirms the page renders without errors
+```
+
+---
+
+## Architecture Overview
+
+```
+violet-app-agent/
+├── apps/agent/
+│ ├── src/violet_app_agent/
+│ │ ├── agent.py # Main agent
+│ │ ├── prompts.py # System prompts
+│ │ ├── subagents/
+│ │ │ ├── __init__.py # Exports subagents
+│ │ │ └── template_designer.py # Template Designer (this feature!)
+│ │ └── templates/
+│ │ └── styles/
+│ │ └── 90s_nostalgia.css # Design system
+│ └── demo/
+│ ├── index.html # Landing page
+│ └── chat_ui_streaming.html # Chat interface
+└── docs/
+ ├── RFC-002-template-ecosystem.md # Design doc
+ └── TEMPLATE_ECOSYSTEM_CONTRIBUTING.md # This file
+```
+
+### Deep Agent Pattern
+
+The agent follows the **DIAGNOSE → MULTI-EXPERT → ARTIFACT-FIRST → DOMAIN-NATIVE** pattern:
+
+```python
+Main Agent (Orchestrator)
+ ├── Architect Subagent (Data model design)
+ ├── Template Designer Subagent ← THIS FEATURE
+ ├── CMS Designer Subagent (Content management)
+ ├── Content Researcher Subagent (Content generation)
+ ├── Deployer Subagent (GitHub deployment)
+ └── Security Subagent (Security checks)
+```
+
+---
+
+## Setup for Development
+
+### Prerequisites
+
+- Python 3.11+
+- Poetry
+- Rails server running on port 5250
+- PostgreSQL database
+
+### Quick Start
+
+```bash
+# Clone and setup
+cd violet_rails/violet-app-agent/apps/agent
+
+# Install dependencies
+poetry install
+
+# Copy environment file
+cp .env.example .env
+# Edit .env with your ANTHROPIC_API_KEY
+
+# Start the agent
+poetry run langgraph dev --port 8123
+
+# In another terminal, start Rails
+cd violet_rails
+bin/rails server -p 5250
+```
+
+### Verify Setup
+
+```bash
+# Check agent is running
+curl http://localhost:8123/health
+
+# Check Rails is running
+curl http://localhost:5250
+```
+
+---
+
+## Key Code Locations
+
+### 1. Template Designer Subagent
+
+**File:** `apps/agent/src/violet_app_agent/subagents/template_designer.py`
+
+```python
+# Verified Liquid tags that exist in Violet Rails
+VERIFIED_LIQUID_TAGS = {
+ "text": "{{ cms:text {identifier} }}",
+ "markdown": "{{ cms:markdown {identifier} }}",
+ "wysiwyg": "{{ cms:wysiwyg {identifier} }}",
+ "asset": "{{ cms:asset {identifier}, as: image }}",
+ "file": "{{ cms:file {identifier} }}",
+ "snippet": "{{ cms:snippet {identifier} }}",
+ "collection": "{{ cms:collection {identifier} | namespace: '{namespace}' }}",
+ "form": "{{ render_form | namespace: '{namespace}' | submit_text: '{submit_text}' }}",
+}
+
+# Tags that DO NOT exist - agent must never use these
+FORBIDDEN_TAGS = {
+ "render_api_form": "Does not exist - use render_form instead",
+ "api_resource": "Does not exist",
+ "render_api": "Does not exist",
+}
+```
+
+### 2. Template Library
+
+```python
+BLOG_TEMPLATES = {
+ "home": {
+ "name": "Blog Home Page",
+ "slots": {
+ "site_title": "Site title displayed in browser tab",
+ "headline": "Main headline (h1)",
+ "tagline": "Subtitle under headline",
+ "footer_text": "Footer copyright/attribution",
+ },
+ "description": "Homepage with hero section and featured posts",
+ },
+ "category": { ... },
+ "post_index": { ... },
+ "post_show": { ... },
+ "write": { ... }, # Has form_namespace for render_form
+ "about": { ... },
+}
+```
+
+### 3. Five Core Tools
+
+| Tool | Purpose |
+|------|---------|
+| `list_templates()` | Show available page templates |
+| `select_template(page_type)` | Get template definition with slots |
+| `get_liquid_tag(tag_type, identifier)` | Return verified Liquid syntax |
+| `generate_styled_page(page_type, slot_values)` | Create HTML with CSS |
+| `verify_page(subdomain, path)` | HTTP check for render errors |
+
+---
+
+## Extension Ideas for Hackathon
+
+### Beginner (2-4 hours)
+
+1. **New Page Template** - Add a "contact" or "faq" template
+2. **CSS Theme Variant** - Create a brutalist or minimalist theme
+3. **New Liquid Tag** - Add a verified tag from Violet Rails
+
+### Intermediate (4-8 hours)
+
+4. **E-commerce Templates** - Product catalog, pricing pages
+5. **SEO Optimizer Subagent** - Meta tags, OG tags, heading analysis
+6. **Accessibility Checker** - WCAG compliance verification
+
+### Advanced (8+ hours)
+
+7. **Multi-page Generator** - Generate entire sites from descriptions
+8. **Template Composition** - Header + body + footer system
+9. **A/B Testing Support** - Template variations with analytics
+
+---
+
+## How to Add a New Template
+
+### Step 1: Define the Template
+
+```python
+# In template_designer.py, add to BLOG_TEMPLATES:
+
+"contact": {
+ "name": "Contact Page",
+ "file": "blog/contact.liquid",
+ "slots": {
+ "page_title": "Page title",
+ "intro_text": "Introduction text",
+ "email": "Contact email address",
+ "phone": "Phone number (optional)",
+ },
+ "form_namespace": "contact", # If it has a form
+ "form_submit_text": "Send Message",
+ "description": "Contact page with form and info",
+},
+```
+
+### Step 2: Add Generation Logic
+
+```python
+# In generate_styled_page(), add a new elif block:
+
+elif page_type == "contact":
+ form_namespace = template.get("form_namespace", "contact")
+ form_submit = template.get("form_submit_text", "Send")
+
+ content_section = f"""
+
+ {slot_values.get("page_title", "Contact Us")}
+ {slot_values.get("intro_text", "")}
+
+
+
+ {{{{ render_form | namespace: '{form_namespace}' | submit_text: '{form_submit}' }}}}
+
+"""
+```
+
+### Step 3: Test It
+
+```python
+# Test in Python REPL
+from violet_app_agent.subagents.template_designer import generate_styled_page
+
+result = generate_styled_page(
+ page_type="contact",
+ slot_values={
+ "page_title": "Get In Touch",
+ "intro_text": "We'd love to hear from you!",
+ "email": "hello@example.com",
+ }
+)
+
+print(result["html"][:500]) # Preview first 500 chars
+```
+
+---
+
+## How to Add a New CSS Theme
+
+### Step 1: Create the CSS File
+
+```css
+/* In templates/styles/brutalist.css */
+
+:root {
+ --bg-cream: #ffffff;
+ --bg-paper: #ffffff;
+ --text-ink: #000000;
+ --text-muted: #333333;
+ --accent-teal: #000000;
+ --accent-coral: #ff0000;
+ --border-soft: #000000;
+
+ --font-serif: "Courier New", monospace;
+ --font-mono: "Courier New", monospace;
+}
+
+/* Override specific components */
+.card {
+ border: 3px solid black;
+ box-shadow: 5px 5px 0 black;
+}
+
+a {
+ text-decoration: none;
+ border-bottom: 2px solid black;
+}
+```
+
+### Step 2: Add Theme Selection
+
+```python
+# In template_designer.py
+
+CSS_THEMES = {
+ "90s_nostalgia": "90s_nostalgia.css",
+ "brutalist": "brutalist.css",
+ "minimalist": "minimalist.css",
+}
+
+def load_css(theme: str = "90s_nostalgia") -> str:
+ css_file = CSS_THEMES.get(theme, "90s_nostalgia.css")
+ css_path = TEMPLATES_DIR / "styles" / css_file
+ return css_path.read_text() if css_path.exists() else ""
+```
+
+### Step 3: Update generate_styled_page
+
+```python
+def generate_styled_page(
+ page_type: str,
+ slot_values: dict[str, str],
+ nav_links: list[dict[str, str]] | None = None,
+ theme: str = "90s_nostalgia", # Add theme parameter
+) -> dict[str, Any]:
+ css = load_css(theme)
+ # ... rest of function
+```
+
+---
+
+## How to Add a New Subagent
+
+### Step 1: Create the Subagent File
+
+```python
+# In subagents/seo_optimizer.py
+
+"""
+SEO Optimizer Subagent
+
+Analyzes and improves page SEO.
+"""
+
+from langchain_core.tools import tool
+from deepagents import SubAgent
+
+@tool
+def analyze_seo(html: str) -> dict:
+ """Analyze HTML for SEO issues."""
+ issues = []
+
+ if "" not in html:
+ issues.append("Missing tag")
+ if 'meta name="description"' not in html:
+ issues.append("Missing meta description")
+ if "" not in html:
+ issues.append("Missing H1 tag")
+
+ return {
+ "score": 100 - (len(issues) * 20),
+ "issues": issues,
+ "recommendations": [f"Fix: {issue}" for issue in issues],
+ }
+
+@tool
+def generate_meta_tags(title: str, description: str, keywords: list[str]) -> str:
+ """Generate SEO meta tags."""
+ return f'''
+{title}
+
+
+
+
+'''
+
+SEO_SUBAGENT_PROMPT = """You are the SEO Optimizer subagent.
+
+Your role is to:
+1. ANALYZE pages for SEO issues
+2. GENERATE meta tags
+3. RECOMMEND improvements
+
+Always prioritize:
+- Clear, descriptive titles
+- Compelling meta descriptions
+- Proper heading hierarchy
+"""
+
+seo_optimizer_subagent: SubAgent = {
+ "name": "seo-optimizer-subagent",
+ "description": "Analyzes and improves page SEO with meta tags and recommendations.",
+ "system_prompt": SEO_SUBAGENT_PROMPT,
+ "tools": [analyze_seo, generate_meta_tags],
+}
+```
+
+### Step 2: Export from __init__.py
+
+```python
+# In subagents/__init__.py
+
+from .seo_optimizer import (
+ seo_optimizer_subagent,
+ analyze_seo,
+ generate_meta_tags,
+)
+
+__all__ = [
+ # ... existing exports
+ "seo_optimizer_subagent",
+ "analyze_seo",
+ "generate_meta_tags",
+]
+```
+
+### Step 3: Register in Main Agent
+
+```python
+# In agent.py
+
+from violet_app_agent.subagents import (
+ # ... existing imports
+ seo_optimizer_subagent,
+ analyze_seo,
+ generate_meta_tags,
+)
+
+def create_violet_app_agent():
+ return create_deep_agent(
+ tools=[
+ # ... existing tools
+ analyze_seo,
+ generate_meta_tags,
+ ],
+ subagents=[
+ # ... existing subagents
+ seo_optimizer_subagent,
+ ],
+ )
+```
+
+---
+
+## Testing Your Changes
+
+### Unit Tests
+
+```python
+# In tests/test_template_designer.py
+
+def test_contact_template():
+ result = generate_styled_page(
+ page_type="contact",
+ slot_values={"page_title": "Contact"}
+ )
+
+ assert "error" not in result
+ assert "Contact" in result["html"]
+ assert "render_form" in result["html"]
+ assert "render_api_form" not in result["html"] # Forbidden!
+
+def test_brutalist_theme():
+ result = generate_styled_page(
+ page_type="home",
+ slot_values={"headline": "Test"},
+ theme="brutalist"
+ )
+
+ assert "Courier New" in result["html"]
+```
+
+### Integration Tests
+
+```python
+def test_full_page_creation():
+ # 1. Generate
+ html_result = generate_styled_page(
+ page_type="home",
+ slot_values={"headline": "Test Site"}
+ )
+
+ # 2. Create (mock or use test subdomain)
+ # create_page(subdomain="test", title="Home", content=html_result["html"])
+
+ # 3. Verify
+ # verify_result = verify_page(subdomain="test", path="/home")
+ # assert verify_result["success"] is True
+```
+
+### Run Tests
+
+```bash
+cd violet-app-agent/apps/agent
+poetry run pytest -v
+poetry run pytest -v tests/test_template_designer.py
+```
+
+---
+
+## Submission Checklist
+
+Before submitting your hackathon project:
+
+- [ ] Code runs without errors
+- [ ] All new templates use only verified Liquid tags
+- [ ] Pages verify successfully with `verify_page()`
+- [ ] Tests pass
+- [ ] README explains what you built
+- [ ] Demo video (2-3 minutes) shows it working
+
+---
+
+## Common Pitfalls
+
+### 1. Hallucinated Liquid Tags
+
+**Wrong:**
+```python
+"{{ render_api_form | namespace: 'contact' }}" # DOESN'T EXIST!
+```
+
+**Right:**
+```python
+"{{ render_form | namespace: 'contact' | submit_text: 'Send' }}"
+```
+
+### 2. Forgetting to Verify
+
+Always call `verify_page()` after creating a page:
+
+```python
+# After create_page()
+result = verify_page(subdomain="my-site", path="/contact")
+if not result["success"]:
+ print(f"Page has errors: {result}")
+```
+
+### 3. Missing Slots
+
+Templates have required slots. Check them:
+
+```python
+template = select_template("home")
+print(template["slots"]) # Shows required slot names
+```
+
+---
+
+## Getting Help
+
+- **GitHub Issues:** [restarone/violet_rails](https://github.com/restarone/violet_rails/issues)
+- **Demo:** Open `demo/index.html` and click "Try It Live"
+- **RFC:** Read `docs/RFC-002-template-ecosystem.md` for design rationale
+
+---
+
+## Design Philosophy
+
+From RFC-002:
+
+> **"Don't Make Me Think Revisited"** (Steve Krug)
+> - Clear, obvious navigation
+> - Every click should be obvious
+> - Room to breathe
+> - Users scan, not read
+
+The 90s nostalgia aesthetic:
+- Cream backgrounds (#f5f0e6)
+- Georgia serif typography
+- Teal (#2a9d8f) and coral (#e76f51) accents
+- CSS Grid layout with generous whitespace
+- Paper-white cards with subtle borders
+
+---
+
+## License
+
+This project is part of Violet Rails, licensed under [MIT License](../../LICENSE).
+
+---
+
+**Built for the Violet Rails Hackathon - "Building with AI" Track**