Files
Codewalkers/docs/preview.md
Lukas May ebe186bd5e feat: Add agent preview integration with auto-teardown and simplified commands
- Add agentId label to preview containers (cw.agent-id) for tracking
- Add startForAgent/stopByAgentId methods to PreviewManager
- Auto-teardown: previews torn down on agent:stopped event
- Conditional preview prompt injection for execute/refine/discuss agents
- Agent-simplified CLI: cw preview start/stop --agent <id>
- cw preview setup command with --auto mode for guided config generation
- hasPreviewConfig hint on cw project register output
- New tRPC procedures: startPreviewForAgent, stopPreviewByAgent
2026-03-05 15:39:15 +01:00

17 KiB

Preview Deployments

apps/server/preview/ — Docker-based preview deployments for reviewing changes in a running application.

Overview

Preview deployments let reviewers spin up a branch in local Docker containers. A single shared Caddy gateway handles subdomain routing for all active previews, accessed at http://<previewId>.localhost:<gatewayPort>.

Two modes:

  • Preview mode: Checks out target branch into a git worktree, builds Docker images, serves production-like output.
  • Dev mode: Mounts the agent's worktree into a container with a dev server image (e.g. node:20-alpine), enabling hot reload.

Auto-start: When a phase enters pending_review, a preview is automatically started for the phase's branch (if the initiative has exactly one project).

Key design decision: No database table. Docker IS the source of truth. Instead of persisting rows, we query Docker directly via compose project names, container labels, and docker compose CLI commands.

Setting Up Preview Deployments for a Project

Prerequisites

  • Docker installed and running (docker --version / docker compose version)
  • Project registered in Codewalkers (cw project add or via the UI)
  • Browser: Chrome or Firefox recommended. Safari requires manual /etc/hosts entries for *.localhost subdomains.

Quick Start

  1. Add a .cw-preview.yml to your project root. This tells Codewalkers how to build and run your app:
version: 1
services:
  app:
    build: .
    port: 3000

That's it for a single-service app with a Dockerfile. Run:

cw preview start --initiative <id> --project <id> --branch <branch>

Open the URL printed in the output (e.g. http://abc123.localhost:9100).

Don't Have a .cw-preview.yml?

The config reader auto-discovers in this order:

  1. .cw-preview.yml — full control (recommended)
  2. docker-compose.yml / compose.yml — uses your existing compose file
  3. Dockerfile — builds a single app service on port 3000

If your project already has a Dockerfile or compose file, previews work out of the box with zero config.

Multi-Service Example

A typical full-stack app with frontend, backend, and database:

version: 1
services:
  frontend:
    build:
      context: .
      dockerfile: apps/web/Dockerfile
    port: 3000
    route: /                      # serves the root path
    healthcheck:
      path: /
      interval: 5s
      retries: 10
    env:
      VITE_API_URL: /api
    dev:                          # optional: used in dev mode
      image: node:20-alpine
      command: npm run dev -- --host 0.0.0.0
      workdir: /app

  backend:
    build:
      context: .
      dockerfile: packages/api/Dockerfile
    port: 8080
    route: /api                   # proxied under /api/*
    healthcheck:
      path: /health
    env:
      DATABASE_URL: postgres://db:5432/app

  db:
    image: postgres:16-alpine
    port: 5432
    internal: true                # not exposed through the proxy
    env:
      POSTGRES_PASSWORD: preview

Requests to http://<id>.localhost:9100/ hit frontend:3000, requests to /api/* hit backend:8080, and db is only reachable by other services.

Config Reference

Each service in .cw-preview.yml supports:

Field Required Description
build yes* Build context — string (".") or object ({context, dockerfile})
image yes* Docker image to pull (e.g. postgres:16-alpine)
port yes** Container port the service listens on
route no URL path prefix for gateway routing (default: /)
internal no If true, not exposed through the proxy (e.g. databases)
healthcheck no {path, interval?, retries?} — polled before marking ready
env no Environment variables passed to the container
volumes no Additional volume mounts
seed no Array of shell commands to run inside the container after health checks pass
dev no Dev mode overrides: {image, command?, workdir?}

* Provide either build or image, not both. ** Required unless internal: true.

Seeding

If a service needs initialization (database migrations, fixture loading, etc.), add a seed array. Commands run inside the container via docker compose exec after all health checks pass, before the preview is marked ready.

services:
  app:
    build: .
    port: 3000
    seed:
      - npm run db:migrate
      - npm run db:seed

Seeds execute in service definition order. Each command has a 5-minute timeout. If any seed command fails (non-zero exit), the preview fails and all containers are cleaned up.

Dev Mode

Dev mode skips the Docker build and instead mounts your source code into a container running a dev server. Useful for hot reload during active development.

To use dev mode, add a dev section to the service:

services:
  app:
    build: .
    port: 3000
    dev:
      image: node:20-alpine          # base image with your runtime
      command: npm run dev -- --host 0.0.0.0   # dev server command
      workdir: /app                   # where source is mounted

Start with:

cw preview start --initiative <id> --project <id> --branch <branch> --mode dev

The project directory is mounted at workdir (default /app). An anonymous volume is created for node_modules to prevent host/container conflicts.

Healthchecks

If a service has a healthcheck, the preview waits for it to respond with HTTP 200 before reporting ready. Without a healthcheck, the service is considered ready as soon as the container starts.

healthcheck:
  path: /health     # required: endpoint to poll
  interval: 5s      # optional: time between checks (default: 5s)
  retries: 10       # optional: max attempts before failing (default: 10)

Auto-Start on Review

When a phase transitions to pending_review, a preview is automatically started if:

  • The initiative has exactly one registered project
  • The project has a discoverable config (.cw-preview.yml, compose file, or Dockerfile)

No manual cw preview start needed — just push your branch and move the phase to review.

Architecture

.cw-previews/
  gateway/
    docker-compose.yml       ← single Caddy container, one port
    Caddyfile                ← regenerated on each preview add/remove
  <previewId>/
    docker-compose.yml       ← per-preview stack (no published ports)
    source/                  ← git worktree (preview mode only)

Docker:
  network: cw-preview-net    ← external, shared by gateway + all previews
  cw-preview-gateway         ← Caddy on one port, subdomain routing
  cw-preview-<id>            ← per-preview compose project (services only)

Routing:
  <previewId>.localhost:<gatewayPort> → cw-preview-<id>-<service>:<port>
PreviewManager
  ├── GatewayManager (shared Caddy gateway lifecycle + Caddyfile generation)
  ├── ConfigReader (discover .cw-preview.yml / compose / Dockerfile)
  ├── ComposeGenerator (generate per-preview docker-compose.yml)
  ├── DockerClient (thin wrapper around docker compose CLI + network ops)
  ├── HealthChecker (poll service healthcheck endpoints via subdomain URL)
  ├── PortAllocator (find next available port 9100-9200 for gateway)
  └── Worktree helper (git worktree add/remove for preview mode)

Lifecycle

  1. Start: ensure gateway → discover config → create worktree (preview) or use provided path (dev) → generate compose → docker compose up --build -d → update gateway routes → health check → run seed commands → emit preview:ready
  2. Stop: docker compose down --volumes --remove-orphans → remove worktree → clean up .cw-previews/<id>/ → update gateway routes → stop gateway if no more previews → emit preview:stopped
  3. List: docker compose ls --filter name=cw-preview → skip gateway project → parse container labels → reconstruct status
  4. Shutdown: stopAll() called on server shutdown — stops all previews, then stops gateway

Gateway

The GatewayManager class manages a single shared Caddy container:

  • ensureGateway() — idempotent. Creates the cw-preview-net Docker network, checks if gateway is already running, allocates a port (9100-9200) if needed, writes compose + Caddyfile, starts Caddy with --watch flag.
  • updateRoutes() — regenerates the full Caddyfile from all active previews. Caddy's --watch flag auto-reloads on file change (no docker exec needed).
  • stopGateway() — composes down the gateway, removes the Docker network, cleans up the gateway directory.

Gateway Caddyfile format:

{
  auto_https off
}

abc123.localhost:9100 {
  handle_path /api/* {
    reverse_proxy cw-preview-abc123-backend:8080
  }
  handle {
    reverse_proxy cw-preview-abc123-frontend:3000
  }
}

xyz789.localhost:9100 {
  handle {
    reverse_proxy cw-preview-xyz789-app:3000
  }
}

Routes are sorted by specificity (longer paths first) to ensure correct matching.

Subdomain Routing

Previews are accessed at http://<previewId>.localhost:<gatewayPort>.

  • Chrome / Firefox: Resolve *.localhost to 127.0.0.1 natively. No DNS config needed.
  • Safari: Requires a /etc/hosts entry: 127.0.0.1 <previewId>.localhost for each preview.

Docker Labels

All preview containers get cw.* labels for metadata retrieval:

Label Purpose
cw.preview "true" — marker for filtering
cw.initiative-id Initiative ID
cw.phase-id Phase ID (optional)
cw.project-id Project ID
cw.branch Branch name
cw.port Gateway port
cw.preview-id Nanoid for this deployment
cw.mode "preview" or "dev"
cw.agent-id Agent ID (optional, set when started via --agent)

Compose Project Naming

  • Gateway: cw-preview-gateway (single instance)
  • Previews: cw-preview-<nanoid> — filtered via docker compose ls --filter name=cw-preview, gateway excluded from listings
  • Container names: cw-preview-<id>-<service> — unique DNS names on the shared network

Networking

  • cw-preview-net — external Docker bridge network shared by gateway + all preview stacks
  • internal — per-preview bridge network for inter-service communication
  • Public services join both networks; internal services (e.g. databases) only join internal

Config Discovery

See Setting Up Preview Deployments above for the full config reference. Discovery order:

  1. .cw-preview.yml — explicit CW preview config (recommended)
  2. docker-compose.yml / compose.yml — existing compose file with gateway network injection
  3. Dockerfile — single-service fallback (builds from ., assumes port 3000)

Module Files

File Purpose
types.ts PreviewConfig, PreviewStatus, labels, constants, dev config types
config-reader.ts Discovery + YAML parsing (including dev section)
compose-generator.ts Per-preview Docker Compose YAML + label generation
gateway.ts GatewayManager class + Caddyfile generation
worktree.ts Git worktree create/remove helpers
docker-client.ts Docker CLI wrapper (execa) + network operations
health-checker.ts Service readiness polling via subdomain URL
port-allocator.ts Port 9100-9200 allocation with TCP bind test
manager.ts PreviewManager class (start/stop/list/status/stopAll + auto-start)
index.ts Barrel exports

Events

Event Payload
preview:building {previewId, initiativeId, branch, gatewayPort, mode, phaseId?}
preview:ready {previewId, initiativeId, branch, gatewayPort, url, mode, phaseId?}
preview:stopped {previewId, initiativeId}
preview:failed {previewId, initiativeId, error}

Auto-Start

PreviewManager.setupEventListeners() listens for phase:pending_review events:

  1. Loads the initiative and its projects
  2. If exactly one project: auto-starts a preview in preview mode
  3. Branch is derived from phaseBranchName(initiative.branch, phase.name)
  4. Errors are caught and logged (best-effort, never blocks the phase transition)

Agent Integration

Agents can spin up and tear down preview deployments using simplified commands that only require their agent ID — the server resolves initiative, project, branch, and mode automatically.

Agent-Simplified Commands

When an agent has a <preview_deployments> section in its prompt (injected automatically if the initiative's project has .cw-preview.yml), it can use:

cw preview start --agent <agentId>    # Server resolves everything, starts dev mode
cw preview stop --agent <agentId>     # Stops all previews for this agent

Prompt Injection

Preview instructions are automatically appended to agent prompts when all conditions are met:

  1. Agent mode is execute, refine, or discuss
  2. Agent has an initiativeId
  3. Initiative has exactly one linked project
  4. Project clone directory contains .cw-preview.yml

The injected <preview_deployments> block includes prefilled cw preview start/stop --agent <agentId> commands.

Agent ID Label

Previews started with --agent receive a cw.agent-id Docker container label. This enables:

  • Auto-teardown: When an agent stops (agent:stopped event), all previews labeled with its ID are automatically torn down (best-effort).
  • Agent-scoped stop: cw preview stop --agent <id> finds and stops previews by label.

Setup Command

cw preview setup prints inline setup instructions for .cw-preview.yml. With --auto --project <id>, it creates an initiative and spawns a refine agent to analyze the project and generate the config file.

tRPC Procedures

Procedure Type Input
startPreview mutation {initiativeId, phaseId?, projectId, branch, mode?, worktreePath?, agentId?}
startPreviewForAgent mutation {agentId}
stopPreview mutation {previewId}
stopPreviewByAgent mutation {agentId}
listPreviews query {initiativeId?}
getPreviewStatus query {previewId}

mode defaults to 'preview'. Set to 'dev' with a worktreePath for dev mode. startPreviewForAgent always uses dev mode.

CLI Commands

cw preview start --initiative <id> --project <id> --branch <branch> [--phase <id>]
cw preview start --agent <id>
cw preview stop <previewId>
cw preview stop --agent <id>
cw preview list [--initiative <id>]
cw preview status <previewId>
cw preview setup [--auto --project <id> [--provider <name>]]

Frontend

The Review tab shows preview status inline:

  • No preview: "Start Preview" button
  • Building: Spinner + "Building preview..."
  • Running: Green dot + http://<id>.localhost:<port> link + Stop button
  • Failed: Error message + Retry button

Polls getPreviewStatus with refetchInterval: 3000 while active.

Container Wiring

  • PreviewManager instantiated in apps/server/container.ts with (projectRepository, eventBus, workspaceRoot, phaseRepository, initiativeRepository)
  • Added to Container interface and toContextDeps()
  • 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

docker build -t cw-preview .
docker run -p 3000:3000 cw-preview
# Wait for startup, then seed:
docker exec <container> sh /app/scripts/seed-preview.sh
# Browse http://localhost:3000

Dependencies

  • js-yaml + @types/js-yaml — for parsing .cw-preview.yml
  • simple-git — for git worktree operations
  • Docker must be installed and running on the host