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

1385 lines
42 KiB
Markdown

# 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
```bash
# 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
```sql
-- 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
```go
// 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
```go
// 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):**
```go
// 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
```go
// 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
````markdown
<!-- .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
```yaml
# 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
```sql
-- 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
```go
// 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
```go
// 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
```go
// 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
```go
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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.
```go
// 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.
```go
// 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.
```go
// 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
```sql
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. │
└─────────────────────────────────────────────────────────────────────────────────┘
```