rdev/app-vision-roadmap.md
jordan 3b35900a2d feat: enterprise worker pool with HTTP sidecar pattern
Implements horizontally-scalable worker pool architecture:
- claudebox-sidecar: HTTP server for Claude Code, git, and SDLC ops
- rdev-worker: standalone worker binary polling rdev-api for tasks
- HTTP client adapter for sidecar communication
- HPA with custom Prometheus metrics for autoscaling
- ServiceMonitor for metrics scraping

Code review fixes applied:
- URL-encode query parameters in GitStatus (Critical #1)
- Remove unused shellQuote function (Critical #2)
- Use stdlib strings.Split/TrimSpace (Critical #3)
- Add version injection via ldflags (Warning #4)
- Add debug logging for swallowed git/sdlc errors (Warning #5, #6)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 16:21:11 -07:00

42 KiB

Orchard Studio: Implementation Roadmap

This roadmap converts the vision into executable phases with clear deliverables.


Phase 0: Engine Validation (Current)

Goal: Prove the SDLC engine can autonomously build complex systems.

Status: In Progress (2 of 4 trees passing)

Slackpath Verification Results

Tree Infrastructure Build Status Notes
slackpath-1 Pass Pass Complete 5 polls (~30s)
slackpath-2 Pass Pass Complete 111 polls (~9 min)
slackpath-3 Pass ⚠️ Timeout Incomplete Build stayed "pending" - worker capacity
slackpath-4 Pass ⚠️ Timeout Incomplete Build stayed "running" - long task

Key Findings

  1. Infrastructure provisioning works reliably - all 4 trees pass wait-infra
  2. SDLC execution works - slackpath-1 and slackpath-2 complete successfully
  3. Worker capacity issue - slackpath-3 never started (stayed "pending")
  4. Long-running task issue - slackpath-4 timed out while still "running"

Remaining Work

Task Status Notes
Worker pool scaling 🔄 In Progress Need capacity for parallel builds
Long-running task handling 🔄 In Progress Extend timeouts or checkpoint
Component templates complete 🔄 In Progress service , worker 🔄, app-react 🔄

Success Criteria

# All 4 slackpath trees complete successfully
./cookbooks/scripts/tree-runner.sh cookbooks/trees/slackpath-1-authenticated-service.yaml  # ✅
./cookbooks/scripts/tree-runner.sh cookbooks/trees/slackpath-2-*.yaml                      # ✅
./cookbooks/scripts/tree-runner.sh cookbooks/trees/slackpath-3-*.yaml                      # 🔄
./cookbooks/scripts/tree-runner.sh cookbooks/trees/slackpath-4-*.yaml                      # 🔄

Exit Condition

All 4 slackpath trees complete autonomously without manual intervention.


Phase 1: Blueprint API (Week 1-2)

Goal: Add Blueprint storage and chat endpoint for Architect conversations.

Week 1: Database & CRUD

Day 1-2: Schema & Migrations

-- migrations/000X_blueprints.up.sql
CREATE TABLE blueprints (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    project_id UUID NOT NULL REFERENCES projects(id),
    feature_name TEXT NOT NULL,
    summary TEXT,
    sections JSONB NOT NULL DEFAULT '{}',
    open_questions JSONB NOT NULL DEFAULT '[]',
    assumptions JSONB NOT NULL DEFAULT '[]',
    ready_to_build BOOLEAN NOT NULL DEFAULT FALSE,
    blockers JSONB NOT NULL DEFAULT '[]',
    status TEXT NOT NULL DEFAULT 'draft',
    built_feature_slug TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE blueprint_messages (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    blueprint_id UUID NOT NULL REFERENCES blueprints(id) ON DELETE CASCADE,
    role TEXT NOT NULL,
    content TEXT NOT NULL,
    blueprint_snapshot JSONB,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_blueprints_project ON blueprints(project_id);
CREATE INDEX idx_blueprint_messages_blueprint ON blueprint_messages(blueprint_id);

Day 3-4: Domain & Repository

// internal/domain/blueprint.go
type Blueprint struct {
    ID            uuid.UUID
    ProjectID     uuid.UUID
    FeatureName   string
    Summary       string
    Sections      BlueprintSections
    OpenQuestions []OpenQuestion
    Assumptions   []Assumption
    ReadyToBuild  bool
    Blockers      []string
    Status        string // draft, building, built, archived
    CreatedAt     time.Time
    UpdatedAt     time.Time
}

type BlueprintSections struct {
    DataModel    DataModelSection
    APIEndpoints APIEndpointsSection
    UIComponents UIComponentsSection
    Dependencies DependenciesSection
}

// internal/port/blueprint.go
type BlueprintRepository interface {
    Create(ctx context.Context, bp *domain.Blueprint) error
    Get(ctx context.Context, id uuid.UUID) (*domain.Blueprint, error)
    Update(ctx context.Context, bp *domain.Blueprint) error
    Delete(ctx context.Context, id uuid.UUID) error
    ListByProject(ctx context.Context, projectID uuid.UUID) ([]domain.Blueprint, error)
    AddMessage(ctx context.Context, msg *domain.BlueprintMessage) error
    GetMessages(ctx context.Context, blueprintID uuid.UUID) ([]domain.BlueprintMessage, error)
}

Day 5: Handler & Routes

// internal/handlers/blueprints.go
func (h *BlueprintHandler) Mount(r chi.Router) {
    r.Route("/projects/{projectId}/blueprints", func(r chi.Router) {
        r.With(auth.RequireScope(auth.ScopeProjectsRead)).Get("/", h.List)
        r.With(auth.RequireScope(auth.ScopeProjectsRead)).Get("/{blueprintId}", h.Get)
        r.With(auth.RequireScope(auth.ScopeProjectsExecute)).Post("/", h.Create)
        r.With(auth.RequireScope(auth.ScopeProjectsExecute)).Delete("/{blueprintId}", h.Delete)
        r.With(auth.RequireScope(auth.ScopeProjectsExecute)).Post("/{blueprintId}/chat", h.Chat)
        r.With(auth.RequireScope(auth.ScopeProjectsExecute)).Post("/{blueprintId}/build", h.Build)
    })
}

Week 2: Architect Integration + Design References

Day 6-7: Architect Service + Reference Handling

The Architect service handles both text conversations and design reference processing.

Reference Capture (parallel track):

// internal/service/reference_service.go
type ReferenceService struct {
    verifyExecutor port.VerifyExecutor  // Reuse Playwright infrastructure
    storage        port.FileStorage
    logger         *slog.Logger
}

func (s *ReferenceService) CaptureURL(ctx context.Context, blueprintID uuid.UUID, url string) (*domain.Reference, error) {
    // 1. Screenshot the URL using Playwright pod
    result, err := s.verifyExecutor.Capture(ctx, domain.VerifySpec{
        URL:       url,
        Viewports: []string{"1920x1080"},  // Desktop only for references
        FullPage:  true,
    })

    // 2. Store screenshot
    path := fmt.Sprintf("/references/%s/%s.png", blueprintID, uuid.New())
    s.storage.Save(ctx, path, result.Screenshots["1920x1080"])

    // 3. Return reference metadata
    return &domain.Reference{
        ID:        uuid.New(),
        Type:      "url",
        Source:    url,
        Thumbnail: path,
    }, nil
}

func (s *ReferenceService) ProcessUpload(ctx context.Context, blueprintID uuid.UUID, data []byte) (*domain.Reference, error) {
    // Handle user-uploaded screenshots
    path := fmt.Sprintf("/references/%s/%s.png", blueprintID, uuid.New())
    s.storage.Save(ctx, path, data)

    return &domain.Reference{
        ID:        uuid.New(),
        Type:      "screenshot",
        Source:    path,
        Thumbnail: path,
    }, nil
}

Day 6-7: Architect Service

// internal/service/architect_service.go
type ArchitectService struct {
    blueprintRepo port.BlueprintRepository
    llmClient     port.LLMClient
    logger        *slog.Logger
}

func (s *ArchitectService) Chat(ctx context.Context, blueprintID uuid.UUID, message string) (*ChatResponse, error) {
    // 1. Load blueprint and conversation history
    bp, _ := s.blueprintRepo.Get(ctx, blueprintID)
    messages, _ := s.blueprintRepo.GetMessages(ctx, blueprintID)

    // 2. Build prompt with Architect persona
    prompt := s.buildArchitectPrompt(bp, messages, message)

    // 3. Call LLM
    response, _ := s.llmClient.Complete(ctx, prompt)

    // 4. Parse structured response
    reply, blueprintUpdate := s.parseResponse(response)

    // 5. Apply blueprint update
    s.applyBlueprintUpdate(bp, blueprintUpdate)

    // 6. Save message and updated blueprint
    s.blueprintRepo.AddMessage(ctx, &domain.BlueprintMessage{...})
    s.blueprintRepo.Update(ctx, bp)

    return &ChatResponse{Reply: reply, Blueprint: bp}, nil
}

Day 8-9: Architect Prompt Engineering

<!-- .claude/agents/architect.md -->

# Architect Agent

You are the Architect for Orchard Studio. Your job is requirements engineering.

## Response Format

ALWAYS respond with valid JSON in this exact format:

```json
{
  "reply": "Your conversational response to the user",
  "blueprint": {
    "feature": "Feature name",
    "summary": "One-line summary",
    "sections": {
      "dataModel": {
        "status": "empty|partial|complete",
        "entities": [...]
      },
      "designSystem": {
        "status": "empty|partial|complete",
        "colors": [...],
        "typography": [...],
        "spacing": [...],
        "inspirationNotes": "..."
      },
      ...
    },
    "references": {
      "items": [...]
    },
    "openQuestions": [...],
    "assumptions": [...],
    "readyToBuild": false,
    "blockers": [...]
  }
}
```

Behavior Rules

  1. Start with the current blueprint (provided below)
  2. Update it based on the user's message
  3. Ask 1-2 clarifying questions if gaps exist
  4. Suggest building when the plan is complete
  5. NEVER write implementation code

Handling Design References

When the user provides a URL or screenshot:

  1. Describe what you observe (conversational, natural language):

    • Layout structure, visual hierarchy
    • Color palette, theme (light/dark)
    • Component patterns you recognize
    • Typography and spacing feel
  2. Ask clarifying questions about intent:

    • "Match exactly or use as inspiration?"
    • "Keep these colors or use your brand?"
    • "Include all elements or simplify for v1?"
  3. Extract structured tokens into designSystem:

    • Colors: Primary, secondary, accent, background, text
    • Typography: Font families, sizes, weights
    • Spacing: Observed rhythm (4px, 8px, 16px, etc.)
    • Border radius, shadows, other patterns
  4. Document in inspirationNotes:

    • Which elements came from which reference
    • What was adapted vs. copied
    • User's stated preferences

The conversation stays loose. The Blueprint stays precise.

Building the Plan Agentively

The transition from loose conversation to structured Blueprint is the core challenge. This is NOT a simple extraction—it requires judgment, interpretation, and iteration.

Key principles:

  1. Don't rush to structure. Let the conversation develop naturally.
  2. Show your work. "I'm inferring spacing of 16px from the card padding I see."
  3. Invite correction. "Does this match what you had in mind?"
  4. Iterate incrementally. Each turn refines the Blueprint slightly.
  5. Distinguish confidence levels. "I'm confident about the 3-tier layout, less sure about the accent color."

The Architect is not a form-filler. It's a collaborator that builds shared understanding.


#### Day 10: Testing & Verification

```bash
# Test conversation flow
curl -X POST $RDEV_API_URL/projects/$PROJECT_ID/blueprints \
  -H "X-API-Key: $RDEV_API_KEY" \
  -d '{"featureName": "Cat Photos"}'

# Returns blueprint_id

curl -X POST $RDEV_API_URL/projects/$PROJECT_ID/blueprints/$BLUEPRINT_ID/chat \
  -H "X-API-Key: $RDEV_API_KEY" \
  -d '{"message": "I want users to post cat photos"}'

# Returns {reply: "Should photos be public or...", blueprint: {...}}

# Test with design reference
curl -X POST $RDEV_API_URL/projects/$PROJECT_ID/blueprints/$BLUEPRINT_ID/chat \
  -H "X-API-Key: $RDEV_API_KEY" \
  -d '{
    "message": "Build a photo grid like this",
    "references": [{"type": "url", "source": "https://unsplash.com/"}]
  }'

# Returns {reply: "I see a masonry grid with rounded corners...", blueprint: {...}}
# Blueprint now includes references.items[] and sections.designSystem

Cookbook Tree: design-from-reference

# cookbooks/trees/design-from-reference.yaml
name: design-from-reference
description: "E2E test: Build feature from visual inspiration"

vars:
    project_name: ""
    reference_url: "https://stripe.com/pricing"

steps:
    create-project:
        action: api
        method: POST
        endpoint: /project
        body:
            name: "{{ .vars.project_name }}"
        outputs:
            - project_id: .data.name

    start-blueprint:
        depends_on: [create-project]
        action: api
        method: POST
        endpoint: "/projects/{{ .outputs.create-project.project_id }}/blueprints"
        body:
            featureName: "Pricing Page"
        outputs:
            - blueprint_id: .data.id

    provide-reference:
        depends_on: [start-blueprint]
        action: api
        method: POST
        endpoint: "/projects/{{ .outputs.create-project.project_id }}/blueprints/{{ .outputs.start-blueprint.blueprint_id }}/chat"
        body:
            message: "Build a pricing page inspired by this"
            references:
                - type: url
                  source: "{{ .vars.reference_url }}"
        outputs:
            - has_design_system: .blueprint.sections.designSystem.status != "empty"

    clarify-intent:
        depends_on: [provide-reference]
        action: api
        method: POST
        endpoint: "/projects/{{ .outputs.create-project.project_id }}/blueprints/{{ .outputs.start-blueprint.blueprint_id }}/chat"
        body:
            message: "Use as inspiration. Match the 3-tier layout but use a light theme with blue accents."

    confirm-plan:
        depends_on: [clarify-intent]
        action: api
        method: POST
        endpoint: "/projects/{{ .outputs.create-project.project_id }}/blueprints/{{ .outputs.start-blueprint.blueprint_id }}/chat"
        body:
            message: "Looks good, build it"

    trigger-build:
        depends_on: [confirm-plan]
        action: api
        method: POST
        endpoint: "/projects/{{ .outputs.create-project.project_id }}/blueprints/{{ .outputs.start-blueprint.blueprint_id }}/build"
        outputs:
            - operation_id: .data.operationId

    wait-build:
        depends_on: [trigger-build]
        action: wait_build
        build_id: "{{ .outputs.trigger-build.operation_id }}"
        max_attempts: 120

teardown:
    - action: api
      method: DELETE
      endpoint: "/project/{{ .outputs.create-project.project_id }}"

Phase 1 Exit Criteria

  • Blueprints table created and migrated
  • CRUD endpoints working
  • Chat endpoint returns structured responses
  • Chat endpoint accepts references[] array (URLs and uploads)
  • URL references auto-screenshot via Playwright
  • Architect describes visual references in conversation
  • Design tokens extracted into sections.designSystem
  • Conversation history persisted
  • readyToBuild computed correctly

Phase 2: Operation Tracking (Week 2-3)

Goal: Move tree/build orchestration from shell scripts to database-tracked operations.

Week 2 (overlap with Phase 1): Schema & Core

Operations Schema

-- migrations/000Y_operations.up.sql
CREATE TABLE operations (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    project_id UUID NOT NULL REFERENCES projects(id),
    blueprint_id UUID REFERENCES blueprints(id),
    operation_type TEXT NOT NULL,
    tree_name TEXT,
    status TEXT NOT NULL DEFAULT 'pending',
    current_phase TEXT,
    progress JSONB NOT NULL DEFAULT '{}',
    result JSONB,
    error TEXT,
    started_at TIMESTAMPTZ,
    completed_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE operation_events (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    operation_id UUID NOT NULL REFERENCES operations(id) ON DELETE CASCADE,
    event_type TEXT NOT NULL,
    payload JSONB NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_operations_project ON operations(project_id);
CREATE INDEX idx_operations_status ON operations(status) WHERE status IN ('pending', 'running');
CREATE INDEX idx_operation_events_operation ON operation_events(operation_id);

Orchestrator Service

// internal/service/orchestrator_service.go
type OrchestratorService struct {
    operationRepo port.OperationRepository
    eventRepo     port.OperationEventRepository
    sdlcService   *SDLCService
    workQueue     port.WorkQueue
    logger        *slog.Logger
}

func (s *OrchestratorService) StartBuild(ctx context.Context, blueprintID uuid.UUID) (*domain.Operation, error) {
    // 1. Create operation record
    op := &domain.Operation{
        ProjectID:     projectID,
        BlueprintID:   &blueprintID,
        OperationType: "build",
        Status:        "pending",
    }
    s.operationRepo.Create(ctx, op)

    // 2. Convert blueprint to SDLC feature spec
    spec := s.blueprintToSpec(blueprint)

    // 3. Enqueue work item with operation ID
    s.workQueue.Enqueue(ctx, &domain.WorkItem{
        Type:        "sdlc",
        OperationID: op.ID,
        Spec:        spec,
    })

    return op, nil
}

func (s *OrchestratorService) EmitEvent(ctx context.Context, opID uuid.UUID, event OperationEvent) error {
    // 1. Persist event
    s.eventRepo.Create(ctx, opID, event)

    // 2. Update operation status
    if event.Type == "phase" {
        s.operationRepo.UpdatePhase(ctx, opID, event.Phase, event.Status)
    }

    return nil
}

Week 3: SSE Streaming

SSE Handler

// internal/handlers/operations_stream.go
func (h *OperationsHandler) Stream(w http.ResponseWriter, r *http.Request) {
    opID := chi.URLParam(r, "operationId")

    // Set SSE headers
    w.Header().Set("Content-Type", "text/event-stream")
    w.Header().Set("Cache-Control", "no-cache")
    w.Header().Set("Connection", "keep-alive")

    flusher, ok := w.(http.Flusher)
    if !ok {
        api.WriteInternalError(w, "streaming not supported")
        return
    }

    // Get existing events (for reconnection)
    events, _ := h.eventRepo.GetByOperation(r.Context(), opID)
    for _, e := range events {
        fmt.Fprintf(w, "event: %s\ndata: %s\n\n", e.Type, e.Payload)
        flusher.Flush()
    }

    // Subscribe to new events
    ch := h.eventBus.Subscribe(opID)
    defer h.eventBus.Unsubscribe(opID, ch)

    for {
        select {
        case event := <-ch:
            fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event.Type, event.Payload)
            flusher.Flush()
            if event.Type == "complete" || event.Type == "error" {
                return
            }
        case <-r.Context().Done():
            return
        }
    }
}

SDLC Executor Instrumentation

// internal/worker/sdlc_executor.go
func (e *SDLCExecutor) Execute(ctx context.Context, task *domain.WorkItem) error {
    // Emit start event
    e.emitEvent(ctx, task.OperationID, OperationEvent{
        Type:    "phase",
        Phase:   "started",
        Status:  "in_progress",
        Message: "Starting build",
    })

    // ... existing execution logic ...

    // Emit phase transitions
    for _, phase := range []string{"spec", "design", "implement", "test", "deploy"} {
        e.emitEvent(ctx, task.OperationID, OperationEvent{
            Type:    "phase",
            Phase:   phase,
            Status:  "in_progress",
            Message: fmt.Sprintf("Starting %s phase", phase),
        })

        // Execute phase...

        e.emitEvent(ctx, task.OperationID, OperationEvent{
            Type:    "phase",
            Phase:   phase,
            Status:  "complete",
            Message: fmt.Sprintf("Completed %s phase", phase),
        })
    }

    // Emit completion
    e.emitEvent(ctx, task.OperationID, OperationEvent{
        Type:    "complete",
        Status:  "success",
        Message: "Build complete",
        URL:     deployedURL,
    })

    return nil
}

Phase 2 Exit Criteria

  • Operations table created and migrated
  • POST /operations starts tracked operation
  • GET /operations/{id} returns current status
  • GET /operations/{id}/stream returns SSE events
  • SDLC executor emits phase events
  • Events persist for replay on reconnect

Phase 3: Blueprint → Build Integration (Week 3)

Goal: Connect "Build It" button to full SDLC execution.

Blueprint to Spec Conversion

// internal/service/architect_service.go
func (s *ArchitectService) Build(ctx context.Context, blueprintID uuid.UUID) (*domain.Operation, error) {
    bp, _ := s.blueprintRepo.Get(ctx, blueprintID)

    // Validate readiness
    if !bp.ReadyToBuild {
        return nil, errors.New("blueprint not ready: " + strings.Join(bp.Blockers, ", "))
    }

    // Convert to spec markdown
    spec := s.renderSpec(bp)

    // Create SDLC feature
    feature, _ := s.sdlcService.CreateFeature(ctx, bp.ProjectID, domain.FeatureRequest{
        Name:    bp.FeatureName,
        Spec:    spec,
    })

    // Link blueprint to feature
    bp.Status = "building"
    bp.BuiltFeatureSlug = feature.Slug
    s.blueprintRepo.Update(ctx, bp)

    // Start operation
    op, _ := s.orchestratorService.StartBuild(ctx, bp.ID, feature.Slug)

    return op, nil
}

func (s *ArchitectService) renderSpec(bp *domain.Blueprint) string {
    tmpl := `# Feature: {{.FeatureName}}

## Summary
{{.Summary}}

## Data Model
{{range .Sections.DataModel.Entities}}
### {{.Name}}
| Field | Type |
|-------|------|
{{range .Fields}}| {{.Name}} | {{.Type}} |
{{end}}
{{end}}

## API Endpoints
{{range .Sections.APIEndpoints.Endpoints}}
- ` + "`{{.Method}} {{.Path}}`" + ` - {{.Description}}
{{end}}

## UI Components
{{range .Sections.UIComponents.Components}}
- **{{.Name}}**: {{.Purpose}}
{{end}}
`
    // ... render template ...
}

Phase 3 Exit Criteria

  • POST /blueprints/{id}/build triggers SDLC feature creation
  • Spec markdown generated from blueprint sections
  • Operation created and trackable
  • Blueprint status updated to "building" → "built"

Phase 4: Frontend (Week 4-5)

Goal: Build the three-pane Studio interface.

Week 4: Project Setup & Core Components

Project Structure

apps/studio/
├── app/
│   ├── layout.tsx
│   ├── page.tsx                    # Template selection
│   ├── auth/
│   │   └── callback/page.tsx       # OAuth callback
│   └── projects/
│       └── [id]/
│           └── page.tsx            # Workspace
├── components/
│   ├── templates/
│   │   └── TemplateCard.tsx
│   ├── workspace/
│   │   ├── ChatPane.tsx
│   │   ├── PlanPane.tsx
│   │   ├── PreviewPane.tsx
│   │   └── BuildProgress.tsx
│   └── ui/                         # shadcn components
├── lib/
│   ├── api.ts                      # rdev-api client
│   ├── sse.ts                      # SSE connection
│   └── store.ts                    # Zustand state
├── tailwind.config.ts
└── package.json

Core State Management

// lib/store.ts
import { create } from "zustand";

interface StudioState {
	// Blueprint state
	blueprint: Blueprint | null;
	messages: Message[];

	// Operation state
	operation: Operation | null;
	events: OperationEvent[];

	// Actions
	sendMessage: (message: string) => Promise<void>;
	startBuild: () => Promise<void>;
	subscribeToOperation: (operationId: string) => void;
}

export const useStudio = create<StudioState>((set, get) => ({
	blueprint: null,
	messages: [],
	operation: null,
	events: [],

	sendMessage: async (message) => {
		const { blueprint } = get();
		const response = await api.chat(blueprint.id, message);
		set({
			blueprint: response.blueprint,
			messages: [
				...get().messages,
				{ role: "user", content: message },
				{ role: "architect", content: response.reply },
			],
		});
	},

	startBuild: async () => {
		const { blueprint } = get();
		const operation = await api.build(blueprint.id);
		set({ operation });
		get().subscribeToOperation(operation.id);
	},

	subscribeToOperation: (operationId) => {
		const eventSource = new EventSource(
			`${API_URL}/operations/${operationId}/stream`,
		);
		eventSource.onmessage = (e) => {
			const event = JSON.parse(e.data);
			set({ events: [...get().events, event] });
			if (event.type === "complete") {
				eventSource.close();
			}
		};
	},
}));

Chat Pane Component

// components/workspace/ChatPane.tsx
export function ChatPane() {
  const { messages, sendMessage, blueprint } = useStudio();
  const [input, setInput] = useState('');

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    if (!input.trim()) return;
    await sendMessage(input);
    setInput('');
  };

  return (
    <div className="flex flex-col h-full">
      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {messages.map((msg, i) => (
          <div key={i} className={cn(
            "p-3 rounded-lg",
            msg.role === 'user' ? "bg-blue-100 ml-8" : "bg-gray-100 mr-8"
          )}>
            {msg.content}
          </div>
        ))}
      </div>

      <form onSubmit={handleSubmit} className="p-4 border-t">
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Describe what you want to build..."
          className="w-full p-2 border rounded"
        />
      </form>
    </div>
  );
}

Plan Pane Component

// components/workspace/PlanPane.tsx
export function PlanPane() {
  const { blueprint } = useStudio();

  if (!blueprint) return <EmptyState />;

  return (
    <div className="p-4 space-y-6">
      <h2 className="text-xl font-bold">{blueprint.feature}</h2>

      {/* Design References */}
      {blueprint.references?.items?.length > 0 && (
        <Section title="Design References" status="complete">
          <div className="flex gap-2 overflow-x-auto">
            {blueprint.references.items.map(ref => (
              <ReferenceCard key={ref.id} reference={ref} />
            ))}
          </div>
        </Section>
      )}

      {/* Design System (extracted from references) */}
      {blueprint.sections.designSystem?.status !== 'empty' && (
        <Section
          title="Design System"
          status={blueprint.sections.designSystem.status}
        >
          <DesignTokens tokens={blueprint.sections.designSystem} />
          {blueprint.sections.designSystem.inspirationNotes && (
            <p className="text-sm text-gray-600 mt-2 italic">
              {blueprint.sections.designSystem.inspirationNotes}
            </p>
          )}
        </Section>
      )}

      <Section
        title="Data Model"
        status={blueprint.sections.dataModel.status}
      >
        {blueprint.sections.dataModel.entities.map(entity => (
          <EntityCard key={entity.name} entity={entity} />
        ))}
      </Section>

      <Section
        title="API Endpoints"
        status={blueprint.sections.apiEndpoints.status}
      >
        {blueprint.sections.apiEndpoints.endpoints.map(ep => (
          <EndpointRow key={ep.path} endpoint={ep} />
        ))}
      </Section>

      <Section
        title="UI Components"
        status={blueprint.sections.uiComponents.status}
      >
        {blueprint.sections.uiComponents.components.map(comp => (
          <ComponentCard key={comp.name} component={comp} />
        ))}
      </Section>

      {blueprint.openQuestions.length > 0 && (
        <div className="bg-yellow-50 p-4 rounded">
          <h3 className="font-semibold text-yellow-800">Open Questions</h3>
          <ul className="list-disc list-inside">
            {blueprint.openQuestions.map(q => (
              <li key={q.id}>{q.question}</li>
            ))}
          </ul>
        </div>
      )}

      <BuildButton
        ready={blueprint.readyToBuild}
        blockers={blueprint.blockers}
      />
    </div>
  );
}

function ReferenceCard({ reference }: { reference: Reference }) {
  return (
    <div className="flex-shrink-0 w-32">
      <img
        src={reference.thumbnail}
        alt={reference.source}
        className="w-full h-20 object-cover rounded border"
      />
      <p className="text-xs text-gray-500 truncate mt-1">
        {reference.type === 'url' ? new URL(reference.source).hostname : 'Uploaded'}
      </p>
    </div>
  );
}

function DesignTokens({ tokens }: { tokens: DesignSystem }) {
  return (
    <div className="space-y-3">
      {tokens.colors?.length > 0 && (
        <div>
          <p className="text-xs font-medium text-gray-500 mb-1">Colors</p>
          <div className="flex gap-1">
            {tokens.colors.map(c => (
              <div
                key={c.name}
                className="w-6 h-6 rounded border"
                style={{ backgroundColor: c.value }}
                title={`${c.name}: ${c.value}`}
              />
            ))}
          </div>
        </div>
      )}
      {tokens.spacing?.length > 0 && (
        <div>
          <p className="text-xs font-medium text-gray-500 mb-1">Spacing</p>
          <p className="text-sm">{tokens.spacing.join('px, ')}px</p>
        </div>
      )}
      {tokens.borderRadius && (
        <div>
          <p className="text-xs font-medium text-gray-500 mb-1">Border Radius</p>
          <p className="text-sm">{tokens.borderRadius}</p>
        </div>
      )}
    </div>
  );
}

Week 5: Polish & Integration

Preview Pane with Build Progress

// components/workspace/PreviewPane.tsx
export function PreviewPane() {
  const { operation, events } = useStudio();
  const [previewUrl, setPreviewUrl] = useState<string>();

  // Update preview URL when build completes
  useEffect(() => {
    const completeEvent = events.find(e => e.type === 'complete');
    if (completeEvent?.url) {
      setPreviewUrl(completeEvent.url);
    }
  }, [events]);

  return (
    <div className="flex flex-col h-full">
      {/* Preview iframe */}
      <div className="flex-1 relative">
        {previewUrl ? (
          <iframe
            src={previewUrl}
            className="w-full h-full border-0"
            sandbox="allow-scripts allow-same-origin allow-forms"
          />
        ) : (
          <div className="flex items-center justify-center h-full bg-gray-50">
            <p className="text-gray-500">Preview will appear after first build</p>
          </div>
        )}
      </div>

      {/* Build progress overlay */}
      {operation && operation.status === 'running' && (
        <BuildProgress events={events} />
      )}
    </div>
  );
}

function BuildProgress({ events }: { events: OperationEvent[] }) {
  const phases = ['spec', 'design', 'implement', 'test', 'deploy'];

  return (
    <div className="absolute inset-0 bg-black/50 flex items-center justify-center">
      <div className="bg-white p-6 rounded-lg shadow-xl w-80">
        <h3 className="font-bold mb-4">Building...</h3>
        <ul className="space-y-2">
          {phases.map(phase => {
            const event = events.find(e => e.phase === phase);
            const status = event?.status || 'pending';

            return (
              <li key={phase} className="flex items-center gap-2">
                {status === 'complete' && <CheckIcon className="text-green-500" />}
                {status === 'in_progress' && <Spinner />}
                {status === 'pending' && <Circle className="text-gray-300" />}
                <span className={cn(
                  status === 'in_progress' && 'font-semibold'
                )}>
                  {phase.charAt(0).toUpperCase() + phase.slice(1)}
                </span>
              </li>
            );
          })}
        </ul>
      </div>
    </div>
  );
}

Phase 4 Exit Criteria

  • Template selection page renders available seeds
  • Three-pane workspace renders correctly
  • Chat sends messages and displays responses
  • Chat supports URL/image reference attachments (drag/drop, paste)
  • Plan pane updates on each chat turn
  • Plan pane displays reference thumbnails
  • Plan pane renders extracted design tokens
  • Build progress shows SSE events
  • Preview iframe loads deployed app
  • Preview refreshes on build complete

Phase 5: Aeries Demo (Week 6)

Goal: Build Aeries (social simulation world) entirely through Studio.

Demo Script

1. Open Orchard Studio
2. Click "Social World" template
3. Wait for project to spawn (shows "Live" badge)

4. Chat with Architect:
   "I want agents that walk around a 2D world and have conversations"

5. Architect asks:
   "How should agents decide who to talk to? Proximity-based, or interest matching?"

6. User: "Proximity - they talk to whoever is nearby"

7. Architect asks:
   "Should conversations be visible to all users, or only when you click on an agent?"

8. User: "Visible as speech bubbles above agents"

9. Architect: "Plan looks complete. Ready to build?"

10. User: "Yes, build it"

11. Watch build progress:
    ✓ Creating spec
    ✓ Designing schema (agents, conversations, positions)
    ✓ Writing handlers (agent CRUD, movement, chat)
    → Running tests
    ○ Deploying

12. Build completes. Preview refreshes.

13. See agents walking and chatting in the preview.

Success Criteria

  • Social World template exists and provisions correctly
  • Architect conversation produces coherent simulation spec
  • Build executes without manual intervention
  • Deployed app shows agents with movement and chat
  • Total time from spawn to working demo < 15 minutes

Phase 6: Platform Services (Parallel Track)

Goal: Add shared platform services that projects can opt into.

This work runs parallel to Phases 1-5. It focuses on the "upgrade existing projects" use case first.

Service Rollout Order

Build infrastructure with the simplest service, then add complexity:

Logging ──► Email ──► Stats ──► Auth
   │          │         │        │
   │          │         │        └── Complex (middleware, user flows)
   │          │         └── Frontend SDK + backend events
   │          └── Simple API, clear success/failure
   └── Pure infrastructure, no user code changes

Phase 6a: Service Infrastructure (Week 3-4)

Goal: Build the provisioning and injection infrastructure.

// internal/port/platform_provisioner.go
type PlatformProvisioner interface {
    Provision(ctx context.Context, req ProvisionRequest) (*ProvisionResult, error)
    Verify(ctx context.Context, projectID string, creds map[string]string) error
    Deprovision(ctx context.Context, projectID string) error
}

Deliverables:

  1. project_services table and domain model
  2. Service catalog (YAML definition of available services)
  3. Provisioner interface and base implementation
  4. Credential encryption/storage
  5. K8s secret injection
  6. Integration PR creation flow
  7. POST /projects/{id}/services endpoint

Phase 6b: Logging Service (Week 4)

Goal: First concrete service - ship logs to centralized Loki.

// internal/adapter/loki/provisioner.go
type LokiProvisioner struct {
    lokiURL string
}

func (p *LokiProvisioner) Provision(ctx context.Context, req ProvisionRequest) (*ProvisionResult, error) {
    tenantID := fmt.Sprintf("project-%s", req.ProjectID)
    return &ProvisionResult{
        Credentials: map[string]string{
            "LOKI_URL":       p.lokiURL,
            "LOKI_TENANT_ID": tenantID,
        },
    }, nil
}

Integration templates:

  • Go: Update slog config to ship to Loki
  • Node: Add pino-loki transport

Exit Criteria:

  • POST /projects/{id}/services with type: logging works
  • Credentials injected into K8s secrets
  • Integration PR created with logger config
  • After merge, logs appear in Loki

Phase 6c: Email Service (Week 5)

Goal: Transactional email via Resend.

// internal/adapter/resend/provisioner.go
type ResendProvisioner struct {
    masterKey string
}

func (p *ResendProvisioner) Provision(ctx context.Context, req ProvisionRequest) (*ProvisionResult, error) {
    // Create scoped API key (or use master with project tracking)
    apiKey := p.createAPIKey(req.ProjectName)
    return &ProvisionResult{
        Credentials: map[string]string{
            "RESEND_API_KEY": apiKey,
        },
        Config: map[string]string{
            "RESEND_FROM_DOMAIN": fmt.Sprintf("%s.threesix.ai", req.ProjectName),
        },
    }, nil
}

Integration templates:

  • Go: internal/email/client.go with Resend SDK
  • Node: lib/email.ts with Resend SDK

Exit Criteria:

  • Email service can be added to existing projects
  • API key provisioned and injected
  • Integration PR includes email client code
  • Test email sends successfully

Phase 6d: Stats Service (Week 5-6)

Goal: Product analytics via PostHog.

Integration templates:

  • Node: AnalyticsProvider component, posthog-js setup
  • Go: Backend event tracking

Exit Criteria:

  • PostHog project created per rdev project
  • Frontend tracking code added
  • Events flowing to PostHog dashboard

Phase 6e: Auth Service (Week 6-7)

Goal: User authentication via Clerk.

This is the most complex service - affects routes, middleware, user model.

Integration templates:

  • Middleware for protected routes
  • ClerkProvider component
  • User model integration

Exit Criteria:

  • Clerk application created per project
  • Auth middleware added
  • Sign-in/sign-up flows working

Phase 7: Dual Environments (Week 6-7)

Goal: Add staging/production environment separation.

Depends on: Phase 6 (services need to be environment-aware)

Week 6: Infrastructure

Deliverables:

  1. DNS: Create both staging.X and X records per project
  2. K8s: Two deployments per project (or two namespaces)
  3. Database: Separate staging/production databases in CockroachDB
  4. Secrets: Environment-scoped secret management
ALTER TABLE projects ADD COLUMN environments JSONB NOT NULL DEFAULT '{
  "staging": {"enabled": true, "deployed_at": null},
  "production": {"enabled": false, "deployed_at": null}
}';

Week 7: Publish Flow

Deliverables:

  1. POST /projects/{id}/publish endpoint
  2. Publish flow: validate → provision → migrate → deploy → verify
  3. Studio UI: Publish button, environment switcher
POST /projects/{id}/publish
{
  "fromEnvironment": "staging",
  "toEnvironment": "production"
}

Exit Criteria:

  • New projects get both environments
  • "Build It" deploys to staging only
  • "Publish" promotes staging to production
  • Services have separate credentials per environment
  • Environment switcher in Preview pane

Milestone Summary

Phase Duration Key Deliverable
0. Engine Now slackpath-1 works
1. Blueprint API Week 1-2 Chat endpoint + storage + design references
2. Operations Week 2-3 SSE streaming + DB tracking
3. Integration Week 3 Blueprint → SDLC → Build
4. Frontend Week 4-5 Three-pane Studio UI (with reference display)
5. Demo Week 6 Aeries via Studio
6. Platform Services Week 3-7 Logging → Email → Stats → Auth (parallel track)
7. Dual Environments Week 6-7 Staging + Production, Publish flow

Total: ~7 weeks to full platform

Parallel Tracks

Week:  1    2    3    4    5    6    7
       │    │    │    │    │    │    │
       ├────┴────┴────┴────┴────┴────┤  Studio Track (Phases 0-5)
       │                              │
       │         ├────┴────┴────┴────┤  Services Track (Phase 6)
       │         │    │    │    │    │
       │         │    │    │    ├────┤  Environments (Phase 7)
  • Studio Track: Core product experience (Blueprint → Build → Preview)
  • Services Track: Platform capabilities (Logging → Email → Stats → Auth)
  • Environments Track: Staging/Production separation (depends on Services)

Design Reference Flow: Integrated into Phase 1 (backend) and Phase 4 (frontend).

Service Rollout: Each service builds on the infrastructure. Start with Logging (simplest), end with Auth (most complex).


Definition of Done: Full Platform

┌─────────────────────────────────────────────────────────────────────────────────┐
│  User clicks "Social World" template                                            │
│                           ↓                                                     │
│  Project spawns: K8s namespace, DB, git repo, DNS                               │
│  Live URL: https://aeries-demo.threesix.ai (shows skeleton)                     │
│                           ↓                                                     │
│  User chats with Architect to define agent simulation                           │
│  Plan pane shows data model, endpoints, components                              │
│                           ↓                                                     │
│  User clicks "Build It"                                                         │
│  Progress shows: spec → design → implement → test → deploy                      │
│                           ↓                                                     │
│  Build completes. Preview refreshes.                                            │
│  User sees agents walking and chatting.                                         │
│                           ↓                                                     │
│  User continues: "Add a weather system"                                         │
│  Architect updates plan. Another build cycle.                                   │
│  Weather appears in preview.                                                    │
│                           ↓                                                     │
│  🎉 Software built through conversation.                                        │
└─────────────────────────────────────────────────────────────────────────────────┘