diff --git a/.cw-preview.yml b/.cw-preview.yml
new file mode 100644
index 0000000..3536fe3
--- /dev/null
+++ b/.cw-preview.yml
@@ -0,0 +1,11 @@
+version: 1
+services:
+ app:
+ build: .
+ port: 3000
+ healthcheck:
+ path: /health
+ interval: 3s
+ retries: 30
+ seed:
+ - sh /app/scripts/seed-preview.sh
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..23ef034
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,12 @@
+node_modules/
+.cw/
+workdir/
+.git/
+apps/server/dist/
+apps/web/dist/
+coverage/
+*.log
+.env*
+.DS_Store
+.screenshots/
+.cw-previews/
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..46bc2b7
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,48 @@
+# Stage 1: Install dependencies (native addons need build tools)
+FROM node:20-alpine AS deps
+RUN apk add --no-cache python3 make g++
+WORKDIR /app
+COPY package.json package-lock.json ./
+COPY apps/web/package.json apps/web/package.json
+COPY packages/shared/package.json packages/shared/package.json
+RUN npm ci
+
+# Stage 2: Build everything (web needs server types via packages/shared)
+FROM deps AS build
+COPY tsconfig.json ./
+COPY apps/server/ apps/server/
+COPY apps/web/ apps/web/
+COPY packages/shared/ packages/shared/
+# Server build (tsc → apps/server/dist/)
+RUN npm run build
+# Web build (vite bundle → apps/web/dist/)
+RUN npx --workspace=apps/web vite build
+
+# Stage 3: Production image
+FROM node:20-alpine AS prod
+RUN apk add --no-cache git curl jq
+COPY --from=caddy:2-alpine /usr/bin/caddy /usr/bin/caddy
+
+WORKDIR /app
+
+# Node modules (includes native better-sqlite3)
+COPY --from=deps /app/node_modules/ node_modules/
+COPY package.json ./
+
+# Server build output + drizzle migrations
+COPY --from=build /app/apps/server/dist/ apps/server/dist/
+COPY apps/server/drizzle/ apps/server/drizzle/
+
+# Web build output
+COPY --from=build /app/apps/web/dist/ apps/web/dist/
+
+# Scripts and config
+COPY scripts/ scripts/
+COPY scripts/Caddyfile /etc/caddy/Caddyfile
+RUN chmod +x scripts/*.sh
+
+# Workspace volume
+RUN mkdir -p /workspace
+
+EXPOSE 3000
+ENTRYPOINT ["sh", "/app/scripts/entrypoint.sh"]
diff --git a/apps/server/preview-seed.ts b/apps/server/preview-seed.ts
new file mode 100644
index 0000000..b5b4307
--- /dev/null
+++ b/apps/server/preview-seed.ts
@@ -0,0 +1,482 @@
+/**
+ * Preview Seed Script
+ *
+ * Populates the database with demo data for preview deployments.
+ * Run with: CW_DB_PATH=/workspace/.cw/cw.db node /app/apps/server/dist/preview-seed.js
+ *
+ * Prints the project ID to stdout so the calling shell script
+ * can derive the clone path.
+ */
+
+import { createDatabase, ensureSchema } from './db/index.js';
+import { createRepositories } from './container.js';
+
+const dbPath = process.env.CW_DB_PATH;
+if (!dbPath) {
+ console.error('CW_DB_PATH environment variable is required');
+ process.exit(1);
+}
+
+const db = createDatabase(dbPath);
+ensureSchema(db);
+const repos = createRepositories(db);
+
+// ─── Project ───
+
+const project = await repos.projectRepository.create({
+ name: 'demo-app',
+ url: 'file:///workspace/demo-repo.git',
+ defaultBranch: 'main',
+});
+
+// ─── Initiative ───
+
+const initiative = await repos.initiativeRepository.create({
+ name: 'Task Manager Redesign',
+ status: 'active',
+ branch: 'cw/task-manager-redesign',
+ executionMode: 'review_per_phase',
+});
+
+await repos.projectRepository.setInitiativeProjects(initiative.id, [project.id]);
+
+// ─── Phases ───
+
+const phase1 = await repos.phaseRepository.create({
+ initiativeId: initiative.id,
+ name: 'Backend API',
+ status: 'completed',
+});
+
+const phase2 = await repos.phaseRepository.create({
+ initiativeId: initiative.id,
+ name: 'UI Overhaul',
+ status: 'pending_review',
+});
+
+const phase3 = await repos.phaseRepository.create({
+ initiativeId: initiative.id,
+ name: 'Testing & Polish',
+ status: 'in_progress',
+});
+
+// ─── Tasks ───
+
+// Phase 1: Backend API (all completed)
+const task1a = await repos.taskRepository.create({
+ phaseId: phase1.id,
+ initiativeId: initiative.id,
+ name: 'Research REST vs GraphQL patterns',
+ category: 'research',
+ status: 'completed',
+ description: 'Evaluate REST and GraphQL approaches for the task management API. Consider developer experience, caching, and real-time requirements.',
+ order: 0,
+ summary: 'Recommended REST with WebSocket subscriptions for real-time. GraphQL adds complexity without clear benefit for this domain.',
+});
+
+const task1b = await repos.taskRepository.create({
+ phaseId: phase1.id,
+ initiativeId: initiative.id,
+ name: 'Implement task CRUD endpoints',
+ category: 'execute',
+ status: 'completed',
+ description: 'Build the core CRUD API for tasks with proper validation and error handling.',
+ order: 1,
+ summary: 'Implemented GET/POST/PUT/DELETE /api/tasks with Zod validation, proper HTTP status codes, and pagination support.',
+});
+
+const task1c = await repos.taskRepository.create({
+ phaseId: phase1.id,
+ initiativeId: initiative.id,
+ name: 'Add input validation middleware',
+ category: 'execute',
+ status: 'completed',
+ description: 'Create reusable validation middleware using Zod schemas for all API endpoints.',
+ order: 2,
+ summary: 'Added validateBody() and validateQuery() middleware with typed error responses.',
+});
+
+// Phase 2: UI Overhaul (all completed)
+const task2a = await repos.taskRepository.create({
+ phaseId: phase2.id,
+ initiativeId: initiative.id,
+ name: 'Design component hierarchy',
+ category: 'plan',
+ status: 'completed',
+ description: 'Plan the component architecture for the redesigned UI, focusing on reusability and maintainability.',
+ order: 0,
+ summary: 'Designed 3-level hierarchy: Layout (Header, Sidebar) → Views (TaskList, TaskDetail) → Primitives (StatusBadge, PriorityTag).',
+});
+
+const task2b = await repos.taskRepository.create({
+ phaseId: phase2.id,
+ initiativeId: initiative.id,
+ name: 'Implement responsive header',
+ category: 'execute',
+ status: 'completed',
+ description: 'Build the responsive header component with search functionality and notification bell.',
+ order: 1,
+ summary: 'Created Header.tsx with search input, notification dropdown (3 mock items), and user avatar. Fully responsive with mobile hamburger menu.',
+});
+
+const task2c = await repos.taskRepository.create({
+ phaseId: phase2.id,
+ initiativeId: initiative.id,
+ name: 'Refactor task list with filters',
+ category: 'execute',
+ status: 'completed',
+ description: 'Rewrite the task list component with status icons, priority badges, and filter controls.',
+ order: 2,
+ summary: 'Refactored TaskList.tsx with table layout, status icons, priority badges, assignee avatars, and due dates. Added TaskFilters.tsx with status/priority filter buttons.',
+});
+
+// Phase 3: Testing & Polish (mixed statuses)
+const task3a = await repos.taskRepository.create({
+ phaseId: phase3.id,
+ initiativeId: initiative.id,
+ name: 'Plan test strategy',
+ category: 'plan',
+ status: 'completed',
+ description: 'Define the testing strategy: unit tests, integration tests, and E2E test coverage targets.',
+ order: 0,
+ summary: 'Defined 3-tier strategy: Vitest unit tests (80% coverage), API integration tests with supertest, and Playwright E2E for critical flows.',
+});
+
+const task3b = await repos.taskRepository.create({
+ phaseId: phase3.id,
+ initiativeId: initiative.id,
+ name: 'Write integration tests',
+ category: 'execute',
+ status: 'in_progress',
+ description: 'Write integration tests for all API endpoints using supertest.',
+ order: 1,
+});
+
+const task3c = await repos.taskRepository.create({
+ phaseId: phase3.id,
+ initiativeId: initiative.id,
+ name: 'Add error handling',
+ category: 'execute',
+ status: 'pending',
+ description: 'Add global error boundary, API error toasts, and retry logic for failed requests.',
+ order: 2,
+});
+
+// ─── Agents ───
+
+const agent1 = await repos.agentRepository.create({
+ name: 'keen-falcon',
+ worktreeId: 'worktrees/keen-falcon',
+ taskId: task1b.id,
+ initiativeId: initiative.id,
+ status: 'stopped',
+ mode: 'execute',
+ provider: 'claude',
+});
+
+const agent2 = await repos.agentRepository.create({
+ name: 'swift-otter',
+ worktreeId: 'worktrees/swift-otter',
+ taskId: task2c.id,
+ initiativeId: initiative.id,
+ status: 'stopped',
+ mode: 'execute',
+ provider: 'claude',
+});
+
+const agent3 = await repos.agentRepository.create({
+ name: 'bright-heron',
+ worktreeId: 'worktrees/bright-heron',
+ taskId: task3b.id,
+ initiativeId: initiative.id,
+ status: 'running',
+ mode: 'execute',
+ provider: 'claude',
+});
+
+// ─── Agent Log Chunks ───
+
+// Agent 1: keen-falcon (completed backend work)
+await repos.logChunkRepository.insertChunk({
+ agentId: agent1.id,
+ agentName: 'keen-falcon',
+ sessionNumber: 1,
+ content: [
+ JSON.stringify({ type: 'system', session_id: 'session-kf-001' }),
+ JSON.stringify({
+ type: 'assistant',
+ message: {
+ content: [
+ { type: 'text', text: "I'll start by reading the existing API structure to understand the current patterns." },
+ { type: 'tool_use', id: 'toolu_kf1', name: 'Read', input: { file_path: '/app/src/lib/api.ts' } },
+ ],
+ },
+ }),
+ JSON.stringify({
+ type: 'user',
+ message: { content: [{ type: 'tool_result', content: "const API_URL = '/api';\n\nexport async function fetchTasks(filter: string) {\n const res = await fetch(`${API_URL}/tasks?filter=${filter}`);\n return res.json();\n}" }] },
+ tool_use_result: { stdout: "const API_URL = '/api';\n\nexport async function fetchTasks(filter: string) {\n const res = await fetch(`${API_URL}/tasks?filter=${filter}`);\n return res.json();\n}" },
+ }),
+ JSON.stringify({
+ type: 'assistant',
+ message: {
+ content: [
+ { type: 'text', text: "Now I'll implement the task CRUD endpoints with proper validation. I'll create a structured API client with error handling." },
+ { type: 'tool_use', id: 'toolu_kf2', name: 'Write', input: { file_path: '/app/src/server/routes/tasks.ts', content: '// CRUD endpoints implementation...' } },
+ ],
+ },
+ }),
+ JSON.stringify({
+ type: 'user',
+ message: { content: [{ type: 'tool_result', content: 'File written successfully' }] },
+ tool_use_result: { stdout: 'File written successfully' },
+ }),
+ JSON.stringify({
+ type: 'assistant',
+ message: {
+ content: [
+ { type: 'text', text: "Let me also add the Zod validation schemas and middleware." },
+ { type: 'tool_use', id: 'toolu_kf3', name: 'Write', input: { file_path: '/app/src/server/middleware/validate.ts', content: '// Validation middleware...' } },
+ ],
+ },
+ }),
+ JSON.stringify({
+ type: 'user',
+ message: { content: [{ type: 'tool_result', content: 'File written successfully' }] },
+ tool_use_result: { stdout: 'File written successfully' },
+ }),
+ JSON.stringify({ type: 'result', is_error: false, total_cost_usd: 0.0847, duration_ms: 32000 }),
+ ].join('\n'),
+});
+
+// Agent 2: swift-otter (completed UI overhaul)
+await repos.logChunkRepository.insertChunk({
+ agentId: agent2.id,
+ agentName: 'swift-otter',
+ sessionNumber: 1,
+ content: [
+ JSON.stringify({ type: 'system', session_id: 'session-so-001' }),
+ JSON.stringify({
+ type: 'assistant',
+ message: {
+ content: [
+ { type: 'text', text: "I'll start by reading the current TaskList component to understand what needs to be refactored." },
+ { type: 'tool_use', id: 'toolu_so1', name: 'Read', input: { file_path: '/app/src/components/TaskList.tsx' } },
+ ],
+ },
+ }),
+ JSON.stringify({
+ type: 'user',
+ message: {
+ content: [{
+ type: 'tool_result',
+ content: "import { useTasks } from '../hooks/useTasks';\n\nexport function TaskList({ filter }) {\n const { tasks, loading } = useTasks(filter);\n if (loading) return
Loading...
;\n return (\n \n {tasks.map(task => (\n - \n {task.title}\n {task.status}\n
\n ))}\n
\n );\n}",
+ }],
+ },
+ tool_use_result: { stdout: "import { useTasks } from '../hooks/useTasks';\n..." },
+ }),
+ JSON.stringify({
+ type: 'assistant',
+ message: {
+ content: [
+ { type: 'text', text: "The current TaskList is very basic — just a plain unordered list with no visual hierarchy. I'll refactor it into a proper table with status icons, priority badges, assignee avatars, and a loading skeleton." },
+ {
+ type: 'tool_use', id: 'toolu_so2', name: 'Edit',
+ input: {
+ file_path: '/app/src/components/TaskList.tsx',
+ old_string: "import { useTasks } from '../hooks/useTasks';",
+ new_string: "import { useTasks } from '../hooks/useTasks';\n\ninterface Task {\n id: string;\n title: string;\n status: 'active' | 'completed' | 'blocked';\n priority: 'high' | 'medium' | 'low';\n assignee?: string;\n}",
+ },
+ },
+ ],
+ },
+ }),
+ JSON.stringify({
+ type: 'user',
+ message: { content: [{ type: 'tool_result', content: 'File edited successfully' }] },
+ tool_use_result: { stdout: 'File edited successfully' },
+ }),
+ JSON.stringify({
+ type: 'assistant',
+ message: {
+ content: [
+ { type: 'text', text: "Now I'll create the TaskFilters component for status and priority filtering." },
+ { type: 'tool_use', id: 'toolu_so3', name: 'Write', input: { file_path: '/app/src/components/TaskFilters.tsx', content: '// TaskFilters component...' } },
+ ],
+ },
+ }),
+ JSON.stringify({
+ type: 'user',
+ message: { content: [{ type: 'tool_result', content: 'File written successfully' }] },
+ tool_use_result: { stdout: 'File written successfully' },
+ }),
+ JSON.stringify({
+ type: 'assistant',
+ message: {
+ content: [
+ { type: 'text', text: "Finally, let me refactor the API client to be type-safe with proper error handling." },
+ { type: 'tool_use', id: 'toolu_so4', name: 'Edit', input: { file_path: '/app/src/lib/api.ts', old_string: "const API_URL = '/api';", new_string: 'class ApiClient {\n private baseUrl: string;\n constructor(baseUrl = \'/api\') { this.baseUrl = baseUrl; }' } },
+ ],
+ },
+ }),
+ JSON.stringify({
+ type: 'user',
+ message: { content: [{ type: 'tool_result', content: 'File edited successfully' }] },
+ tool_use_result: { stdout: 'File edited successfully' },
+ }),
+ JSON.stringify({ type: 'result', is_error: false, total_cost_usd: 0.1234, duration_ms: 45000 }),
+ ].join('\n'),
+});
+
+// Agent 3: bright-heron (in-progress, no result line)
+await repos.logChunkRepository.insertChunk({
+ agentId: agent3.id,
+ agentName: 'bright-heron',
+ sessionNumber: 1,
+ content: [
+ JSON.stringify({ type: 'system', session_id: 'session-bh-001' }),
+ JSON.stringify({
+ type: 'assistant',
+ message: {
+ content: [
+ { type: 'text', text: "I'll start writing integration tests for the task CRUD endpoints. Let me first check the existing test setup." },
+ { type: 'tool_use', id: 'toolu_bh1', name: 'Bash', input: { command: 'ls src/test/', description: 'Check existing test files' } },
+ ],
+ },
+ }),
+ JSON.stringify({
+ type: 'user',
+ message: { content: [{ type: 'tool_result', content: 'setup.ts\nfixtures/' }] },
+ tool_use_result: { stdout: 'setup.ts\nfixtures/' },
+ }),
+ JSON.stringify({
+ type: 'assistant',
+ message: {
+ content: [
+ { type: 'text', text: "Good, there's a test setup file and fixtures directory. Let me read the setup to understand the test configuration." },
+ { type: 'tool_use', id: 'toolu_bh2', name: 'Read', input: { file_path: '/app/src/test/setup.ts' } },
+ ],
+ },
+ }),
+ JSON.stringify({
+ type: 'user',
+ message: { content: [{ type: 'tool_result', content: "import { beforeAll, afterAll } from 'vitest';\nimport { createTestServer } from './fixtures/server';\n\nlet server;\nbeforeAll(async () => { server = await createTestServer(); });\nafterAll(() => server?.close());" }] },
+ tool_use_result: { stdout: "import { beforeAll, afterAll } from 'vitest';\n..." },
+ }),
+ JSON.stringify({
+ type: 'assistant',
+ message: {
+ content: [
+ { type: 'text', text: "Now I'll write the integration tests for the tasks API. Starting with the CRUD operations..." },
+ { type: 'tool_use', id: 'toolu_bh3', name: 'Write', input: { file_path: '/app/src/test/tasks.test.ts', content: '// Integration tests...' } },
+ ],
+ },
+ }),
+ JSON.stringify({
+ type: 'user',
+ message: { content: [{ type: 'tool_result', content: 'File written successfully' }] },
+ tool_use_result: { stdout: 'File written successfully' },
+ }),
+ ].join('\n'),
+});
+
+// ─── Root Page ───
+
+const pageContent = {
+ type: 'doc',
+ content: [
+ {
+ type: 'heading',
+ attrs: { level: 1 },
+ content: [{ type: 'text', text: 'Task Manager Redesign' }],
+ },
+ {
+ type: 'paragraph',
+ content: [
+ {
+ type: 'text',
+ text: 'A comprehensive redesign of the task management application, focusing on improved user experience, type safety, and modern UI patterns.',
+ },
+ ],
+ },
+ {
+ type: 'heading',
+ attrs: { level: 2 },
+ content: [{ type: 'text', text: 'Goals' }],
+ },
+ {
+ type: 'bulletList',
+ content: [
+ {
+ type: 'listItem',
+ content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Responsive layout with mobile-first design' }] }],
+ },
+ {
+ type: 'listItem',
+ content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Type-safe API client with proper error handling' }] }],
+ },
+ {
+ type: 'listItem',
+ content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Status icons and priority badges for visual clarity' }] }],
+ },
+ {
+ type: 'listItem',
+ content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Comprehensive test coverage (unit, integration, E2E)' }] }],
+ },
+ ],
+ },
+ {
+ type: 'heading',
+ attrs: { level: 2 },
+ content: [{ type: 'text', text: 'Progress' }],
+ },
+ {
+ type: 'paragraph',
+ content: [
+ {
+ type: 'text',
+ text: 'Backend API is complete with full CRUD endpoints and validation middleware. The UI overhaul phase is in review — header, task list, and filters have been refactored. Testing phase is actively in progress with integration tests being written.',
+ },
+ ],
+ },
+ ],
+};
+
+await repos.pageRepository.create({
+ initiativeId: initiative.id,
+ title: 'Task Manager Redesign',
+ content: JSON.stringify(pageContent),
+ sortOrder: 0,
+});
+
+// ─── Review Comments (on Phase 2) ───
+
+await repos.reviewCommentRepository.create({
+ phaseId: phase2.id,
+ filePath: 'src/components/TaskList.tsx',
+ lineNumber: 12,
+ lineType: 'added',
+ body: 'Should we memoize the STATUS_ICONS and PRIORITY_COLORS objects since they are static? Moving them outside the component is good, but wrapping the component in React.memo might help with re-renders.',
+ author: 'you',
+});
+
+await repos.reviewCommentRepository.create({
+ phaseId: phase2.id,
+ filePath: 'src/components/Header.tsx',
+ lineNumber: 8,
+ lineType: 'added',
+ body: 'The notifications are hardcoded. For the demo this is fine, but we should add a TODO to wire this up to a real notification system.',
+ author: 'you',
+});
+
+await repos.reviewCommentRepository.create({
+ phaseId: phase2.id,
+ filePath: 'src/lib/api.ts',
+ lineNumber: 25,
+ lineType: 'added',
+ body: 'Nice error handling pattern. Could we also add request timeout support? Long-running requests should fail gracefully.',
+ author: 'you',
+});
+
+// Print project ID for the shell script
+process.stdout.write(project.id);
diff --git a/docs/preview.md b/docs/preview.md
index 66e9d03..c1482d7 100644
--- a/docs/preview.md
+++ b/docs/preview.md
@@ -358,6 +358,47 @@ Polls `getPreviewStatus` with `refetchInterval: 3000` while active.
- `GracefulShutdown` calls `previewManager.stopAll()` during shutdown
- `requirePreviewManager(ctx)` helper in `apps/server/trpc/routers/_helpers.ts`
+## Codewalkers Self-Preview
+
+Codewalkers itself has a Dockerfile, `.cw-preview.yml`, and seed script for preview deployments. This lets you demo the full app — initiatives, phases, tasks, agents with output, pages, and review tab with real git diffs.
+
+### Files
+
+| File | Purpose |
+|------|---------|
+| `Dockerfile` | Multi-stage: deps → build (server+web) → production (Node + Caddy) |
+| `.cw-preview.yml` | Preview config: build, healthcheck, seed |
+| `scripts/Caddyfile` | Caddy config: SPA file server + tRPC reverse proxy |
+| `scripts/entrypoint.sh` | Init workspace, start backend, run Caddy |
+| `scripts/seed-preview.sh` | Create demo git repo + run Node.js seed |
+| `apps/server/preview-seed.ts` | Populate DB with demo initiative, phases, tasks, agents, log chunks, pages, review comments |
+
+### Container Architecture
+
+Single container running two processes:
+- **Node.js backend** on port 3847 (tRPC API + health endpoint)
+- **Caddy** on port 3000 (SPA file server, reverse proxy `/trpc` and `/health` to backend)
+
+### Seed Data
+
+The seed creates a "Task Manager Redesign" initiative with:
+- 3 phases (completed, pending_review, in_progress)
+- 9 tasks across phases
+- 3 agents with JSONL log output
+- Root page with Tiptap content
+- 3 review comments on the pending_review phase
+- Git repo with 3 branches and real commit diffs for the review tab
+
+### Manual Testing
+
+```sh
+docker build -t cw-preview .
+docker run -p 3000:3000 cw-preview
+# Wait for startup, then seed:
+docker exec sh /app/scripts/seed-preview.sh
+# Browse http://localhost:3000
+```
+
## Dependencies
- `js-yaml` + `@types/js-yaml` — for parsing `.cw-preview.yml`
diff --git a/scripts/Caddyfile b/scripts/Caddyfile
new file mode 100644
index 0000000..6d5222e
--- /dev/null
+++ b/scripts/Caddyfile
@@ -0,0 +1,15 @@
+:3000 {
+ handle /trpc/* {
+ reverse_proxy localhost:3847
+ }
+
+ handle /health {
+ reverse_proxy localhost:3847
+ }
+
+ handle {
+ root * /app/apps/web/dist
+ try_files {path} /index.html
+ file_server
+ }
+}
diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh
new file mode 100755
index 0000000..81134be
--- /dev/null
+++ b/scripts/entrypoint.sh
@@ -0,0 +1,34 @@
+#!/bin/sh
+set -e
+
+# 1. Initialize workspace
+mkdir -p /workspace/.cw
+echo '{"version":1}' > /workspace/.cwrc
+
+# 2. Git globals
+git config --global user.email "preview@codewalkers.dev"
+git config --global user.name "Codewalkers Preview"
+git config --global protocol.file.allow always
+git config --global init.defaultBranch main
+
+# 3. Start backend
+cd /workspace
+CW_DB_PATH=/workspace/.cw/cw.db node /app/apps/server/dist/bin/cw.js --server &
+BACKEND_PID=$!
+
+# 4. Wait for health
+echo "Waiting for backend to start..."
+TRIES=0
+MAX_TRIES=60
+until curl -sf http://localhost:3847/health > /dev/null 2>&1; do
+ TRIES=$((TRIES + 1))
+ if [ "$TRIES" -ge "$MAX_TRIES" ]; then
+ echo "Backend failed to start after ${MAX_TRIES}s"
+ exit 1
+ fi
+ sleep 1
+done
+echo "Backend is healthy"
+
+# 5. Run Caddy in foreground (receives signals)
+exec caddy run --config /etc/caddy/Caddyfile
diff --git a/scripts/seed-preview.sh b/scripts/seed-preview.sh
new file mode 100755
index 0000000..5dfd305
--- /dev/null
+++ b/scripts/seed-preview.sh
@@ -0,0 +1,577 @@
+#!/bin/sh
+set -e
+
+echo "=== Creating demo git repository ==="
+
+BARE_REPO="/workspace/demo-repo.git"
+TEMP_DIR=$(mktemp -d)
+
+# Initialize bare repo
+git init --bare "$BARE_REPO"
+
+cd "$TEMP_DIR"
+git init -b main
+git remote add origin "$BARE_REPO"
+
+# ─── main branch: initial task manager app ───
+
+cat > README.md << 'READMEEOF'
+# Task Manager
+
+A modern task management application built with React and TypeScript.
+
+## Features
+
+- Create, update, and delete tasks
+- Filter by status and priority
+- Real-time updates
+- Responsive design
+
+## Getting Started
+
+```bash
+npm install
+npm run dev
+```
+READMEEOF
+
+cat > package.json << 'PKGEOF'
+{
+ "name": "task-manager",
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build"
+ },
+ "dependencies": {
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ }
+}
+PKGEOF
+
+mkdir -p src/components src/hooks src/lib
+
+cat > src/app.tsx << 'APPEOF'
+import { useState } from 'react';
+import { TaskList } from './components/TaskList';
+import { Sidebar } from './components/Sidebar';
+
+export function App() {
+ const [filter, setFilter] = useState('all');
+
+ return (
+
+
+
+ Task Manager
+
+
+
+ );
+}
+APPEOF
+
+cat > src/components/TaskList.tsx << 'TLEOF'
+import { useTasks } from '../hooks/useTasks';
+
+interface TaskListProps {
+ filter: string;
+}
+
+export function TaskList({ filter }: TaskListProps) {
+ const { tasks, loading } = useTasks(filter);
+
+ if (loading) return Loading...
;
+
+ return (
+
+ {tasks.map(task => (
+ -
+ {task.title}
+ {task.status}
+
+ ))}
+
+ );
+}
+TLEOF
+
+cat > src/components/Sidebar.tsx << 'SBEOF'
+interface SidebarProps {
+ filter: string;
+ onFilterChange: (filter: string) => void;
+}
+
+export function Sidebar({ filter, onFilterChange }: SidebarProps) {
+ const filters = ['all', 'active', 'completed'];
+
+ return (
+
+ );
+}
+SBEOF
+
+cat > src/hooks/useTasks.ts << 'HTEOF'
+import { useState, useEffect } from 'react';
+import { fetchTasks } from '../lib/api';
+
+export function useTasks(filter: string) {
+ const [tasks, setTasks] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ fetchTasks(filter).then(data => {
+ setTasks(data);
+ setLoading(false);
+ });
+ }, [filter]);
+
+ return { tasks, loading };
+}
+HTEOF
+
+cat > src/lib/api.ts << 'APIEOF'
+const API_URL = '/api';
+
+export async function fetchTasks(filter: string) {
+ const res = await fetch(`${API_URL}/tasks?filter=${filter}`);
+ return res.json();
+}
+
+export async function createTask(title: string) {
+ const res = await fetch(`${API_URL}/tasks`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ title }),
+ });
+ return res.json();
+}
+APIEOF
+
+git add -A
+git commit -m "feat: initial task manager setup"
+git push origin main
+
+# ─── initiative branch (same as main, no extra commits) ───
+
+git checkout -b cw/task-manager-redesign
+git push origin cw/task-manager-redesign
+
+# ─── phase branch: UI overhaul with 3 commits ───
+
+git checkout -b cw/task-manager-redesign-phase-ui-overhaul
+
+# Commit 1: responsive header with search and notifications
+mkdir -p src/components
+
+cat > src/components/Header.tsx << 'HDREOF'
+import { useState } from 'react';
+
+interface HeaderProps {
+ onSearch: (query: string) => void;
+}
+
+export function Header({ onSearch }: HeaderProps) {
+ const [query, setQuery] = useState('');
+ const [notifications] = useState([
+ { id: 1, text: 'Task "Deploy v2" completed', unread: true },
+ { id: 2, text: 'New comment on "API redesign"', unread: true },
+ { id: 3, text: 'Sprint review tomorrow', unread: false },
+ ]);
+
+ const unreadCount = notifications.filter(n => n.unread).length;
+
+ return (
+
+ );
+}
+HDREOF
+
+cat > src/components/TaskFilters.tsx << 'TFEOF'
+interface TaskFiltersProps {
+ activeFilter: string;
+ onFilterChange: (filter: string) => void;
+ activePriority: string;
+ onPriorityChange: (priority: string) => void;
+}
+
+export function TaskFilters({
+ activeFilter,
+ onFilterChange,
+ activePriority,
+ onPriorityChange,
+}: TaskFiltersProps) {
+ const statuses = ['all', 'active', 'completed', 'blocked'];
+ const priorities = ['all', 'high', 'medium', 'low'];
+
+ return (
+
+
+
+
+ {statuses.map(s => (
+
+ ))}
+
+
+
+
+
+ {priorities.map(p => (
+
+ ))}
+
+
+
+ );
+}
+TFEOF
+
+# Update app.tsx to use Header
+cat > src/app.tsx << 'APPEOF2'
+import { useState } from 'react';
+import { Header } from './components/Header';
+import { TaskList } from './components/TaskList';
+import { TaskFilters } from './components/TaskFilters';
+import { Sidebar } from './components/Sidebar';
+
+export function App() {
+ const [filter, setFilter] = useState('all');
+ const [priority, setPriority] = useState('all');
+ const [searchQuery, setSearchQuery] = useState('');
+
+ return (
+
+ );
+}
+APPEOF2
+
+git add -A
+git commit -m "feat: add responsive header with search and notifications"
+
+# Commit 2: enhance task list with status icons and priorities
+cat > src/components/TaskList.tsx << 'TL2EOF'
+import { useTasks } from '../hooks/useTasks';
+
+interface Task {
+ id: string;
+ title: string;
+ status: 'active' | 'completed' | 'blocked';
+ priority: 'high' | 'medium' | 'low';
+ assignee?: string;
+ dueDate?: string;
+}
+
+interface TaskListProps {
+ filter: string;
+}
+
+const STATUS_ICONS: Record = {
+ active: '🔵',
+ completed: '✅',
+ blocked: '🔴',
+};
+
+const PRIORITY_COLORS: Record = {
+ high: 'priority-high',
+ medium: 'priority-medium',
+ low: 'priority-low',
+};
+
+export function TaskList({ filter }: TaskListProps) {
+ const { tasks, loading } = useTasks(filter);
+
+ if (loading) {
+ return (
+
+ {[1, 2, 3].map(i => (
+
+ ))}
+
+ );
+ }
+
+ if (tasks.length === 0) {
+ return (
+
+
No tasks found
+
+
+ );
+ }
+
+ return (
+
+
+ Task
+ Priority
+ Assignee
+ Due Date
+ Status
+
+ {(tasks as Task[]).map(task => (
+
+
+ {STATUS_ICONS[task.status]}
+ {task.title}
+
+
+ {task.priority}
+
+
+ {task.assignee ? (
+ {task.assignee.slice(0, 2).toUpperCase()}
+ ) : (
+ —
+ )}
+
+
{task.dueDate ?? '—'}
+
+ {task.status}
+
+
+ ))}
+
+ );
+}
+TL2EOF
+
+git add -A
+git commit -m "refactor: enhance task list with status icons and priorities"
+
+# Commit 3: type-safe API client with error handling
+cat > src/lib/api.ts << 'API2EOF'
+interface ApiError {
+ message: string;
+ status: number;
+}
+
+interface Task {
+ id: string;
+ title: string;
+ status: 'active' | 'completed' | 'blocked';
+ priority: 'high' | 'medium' | 'low';
+ assignee?: string;
+ dueDate?: string;
+}
+
+interface CreateTaskInput {
+ title: string;
+ priority?: Task['priority'];
+ assignee?: string;
+ dueDate?: string;
+}
+
+class ApiClient {
+ private baseUrl: string;
+
+ constructor(baseUrl = '/api') {
+ this.baseUrl = baseUrl;
+ }
+
+ private async request(path: string, options?: RequestInit): Promise {
+ const response = await fetch(`${this.baseUrl}${path}`, {
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...options?.headers,
+ },
+ });
+
+ if (!response.ok) {
+ const error: ApiError = {
+ message: `Request failed: ${response.statusText}`,
+ status: response.status,
+ };
+ throw error;
+ }
+
+ return response.json();
+ }
+
+ async getTasks(filter?: string): Promise {
+ const params = filter && filter !== 'all' ? `?filter=${filter}` : '';
+ return this.request(`/tasks${params}`);
+ }
+
+ async getTask(id: string): Promise {
+ return this.request(`/tasks/${id}`);
+ }
+
+ async createTask(input: CreateTaskInput): Promise {
+ return this.request('/tasks', {
+ method: 'POST',
+ body: JSON.stringify(input),
+ });
+ }
+
+ async updateTask(id: string, updates: Partial): Promise {
+ return this.request(`/tasks/${id}`, {
+ method: 'PATCH',
+ body: JSON.stringify(updates),
+ });
+ }
+
+ async deleteTask(id: string): Promise {
+ await this.request(`/tasks/${id}`, { method: 'DELETE' });
+ }
+}
+
+export const api = new ApiClient();
+API2EOF
+
+cat > src/hooks/useTasks.ts << 'HT2EOF'
+import { useState, useEffect, useCallback } from 'react';
+import { api } from '../lib/api';
+
+interface Task {
+ id: string;
+ title: string;
+ status: 'active' | 'completed' | 'blocked';
+ priority: 'high' | 'medium' | 'low';
+ assignee?: string;
+ dueDate?: string;
+}
+
+interface UseTasksResult {
+ tasks: Task[];
+ loading: boolean;
+ error: string | null;
+ refresh: () => void;
+ createTask: (title: string) => Promise;
+ deleteTask: (id: string) => Promise;
+}
+
+export function useTasks(filter: string): UseTasksResult {
+ const [tasks, setTasks] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const loadTasks = useCallback(async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ const data = await api.getTasks(filter);
+ setTasks(data);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to load tasks');
+ } finally {
+ setLoading(false);
+ }
+ }, [filter]);
+
+ useEffect(() => {
+ loadTasks();
+ }, [loadTasks]);
+
+ const createTask = useCallback(async (title: string) => {
+ await api.createTask({ title });
+ await loadTasks();
+ }, [loadTasks]);
+
+ const deleteTask = useCallback(async (id: string) => {
+ await api.deleteTask(id);
+ await loadTasks();
+ }, [loadTasks]);
+
+ return { tasks, loading, error, refresh: loadTasks, createTask, deleteTask };
+}
+HT2EOF
+
+git add -A
+git commit -m "refactor: type-safe API client with error handling"
+
+# Push all branches
+git push origin cw/task-manager-redesign-phase-ui-overhaul
+
+# Clean up
+cd /
+rm -rf "$TEMP_DIR"
+
+echo "=== Demo git repository created at $BARE_REPO ==="
+
+# ─── Step 2: Run Node.js seed ───
+
+echo "=== Running database seed ==="
+PROJECT_ID=$(CW_DB_PATH=/workspace/.cw/cw.db node /app/apps/server/dist/preview-seed.js)
+echo "=== Seed complete, project ID: $PROJECT_ID ==="
+
+# ─── Step 3: Set up local branches in the server's clone dir ───
+
+CLONE_DIR="/workspace/repos/demo-app-${PROJECT_ID}"
+echo "=== Cloning demo repo to $CLONE_DIR ==="
+
+mkdir -p /workspace/repos
+git clone "$BARE_REPO" "$CLONE_DIR"
+
+echo "=== Setting up local branches in $CLONE_DIR ==="
+
+cd "$CLONE_DIR"
+git checkout -b cw/task-manager-redesign origin/cw/task-manager-redesign
+git checkout -b cw/task-manager-redesign-phase-ui-overhaul origin/cw/task-manager-redesign-phase-ui-overhaul
+git checkout main
+
+echo "=== Preview seed complete ==="