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>
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
- Infrastructure provisioning works reliably - all 4 trees pass
wait-infra - SDLC execution works - slackpath-1 and slackpath-2 complete successfully
- Worker capacity issue - slackpath-3 never started (stayed "pending")
- 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
- Start with the current blueprint (provided below)
- Update it based on the user's message
- Ask 1-2 clarifying questions if gaps exist
- Suggest building when the plan is complete
- NEVER write implementation code
Handling Design References
When the user provides a URL or screenshot:
-
Describe what you observe (conversational, natural language):
- Layout structure, visual hierarchy
- Color palette, theme (light/dark)
- Component patterns you recognize
- Typography and spacing feel
-
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?"
-
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
-
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:
- Don't rush to structure. Let the conversation develop naturally.
- Show your work. "I'm inferring spacing of 16px from the card padding I see."
- Invite correction. "Does this match what you had in mind?"
- Iterate incrementally. Each turn refines the Blueprint slightly.
- 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
readyToBuildcomputed 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 /operationsstarts tracked operationGET /operations/{id}returns current statusGET /operations/{id}/streamreturns 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}/buildtriggers 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:
project_servicestable and domain model- Service catalog (YAML definition of available services)
- Provisioner interface and base implementation
- Credential encryption/storage
- K8s secret injection
- Integration PR creation flow
POST /projects/{id}/servicesendpoint
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}/serviceswithtype: loggingworks- 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.gowith Resend SDK - Node:
lib/email.tswith 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:
- DNS: Create both
staging.XandXrecords per project - K8s: Two deployments per project (or two namespaces)
- Database: Separate staging/production databases in CockroachDB
- 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:
POST /projects/{id}/publishendpoint- Publish flow: validate → provision → migrate → deploy → verify
- 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. │
└─────────────────────────────────────────────────────────────────────────────────┘