- Add feature-dev-test.sh: full 10-step E2E test for SDLC + Claude Code workflow - Update feature-development.md cookbook with complete workflow documentation - Fix SDLC orchestrator and project management handler improvements - Update scaffold-test.sh with minor fixes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
32 KiB
Feature Development Cookbook
Complete workflow for building features using rdev composable monorepo templates with chassis patterns, design system, and auth integration.
Overview
This cookbook documents the end-to-end workflow for developing features in a composable monorepo project. There are two approaches:
- SDLC-Driven (Recommended) - Structured workflow using the SDLC system and Claude Code
- Manual - Traditional development with manual planning and implementation
The SDLC-driven approach uses skeleton commands that Claude executes inside the project pod, producing structured artifacts (spec, design, tasks) that can be approved and tracked.
SDLC Feature Development Workflow
──────────────────────────────────
1. Create Feature → POST /sdlc/features
2. Generate Spec → Claude runs /spec-feature
3. Approve Spec → POST /sdlc/features/{slug}/artifacts/spec/approve
4. Generate Design → Claude runs /design-feature
5. Approve Design → POST /sdlc/features/{slug}/artifacts/design/approve
6. Break Down Tasks → Claude runs /breakdown-feature
7. Approve Tasks → POST /sdlc/features/{slug}/artifacts/tasks/approve
8. Implement Tasks → Claude runs /implement-task for each
9. Review & Merge → Claude runs /review-feature, /merge-feature
Quick Start: API-Driven Feature Development
This section shows the minimal API calls to develop a feature using Claude Code and the SDLC system.
Prerequisites
export RDEV_API_URL="https://rdev.masq-ops.orchard9.ai"
export RDEV_API_KEY="<your-api-key>"
1. Create Feature
curl -X POST "$RDEV_API_URL/projects/myapp/sdlc/features" \
-H "X-API-Key: $RDEV_API_KEY" \
-H "Content-Type: application/json" \
-d '{"slug": "add-hello", "title": "Add /hello endpoint"}'
2. Generate Spec with Claude
curl -X POST "$RDEV_API_URL/projects/myapp/builds" \
-H "X-API-Key: $RDEV_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "/spec-feature add-hello",
"auto_commit": true,
"auto_push": true,
"git_clone_url": "https://git.threesix.ai/jordan/myapp.git"
}'
# Returns: {"data": {"task_id": "abc123", ...}}
3. Check Build Status
curl "$RDEV_API_URL/builds/abc123" \
-H "X-API-Key: $RDEV_API_KEY"
# Wait for status: "completed"
4. Check Classifier for Next Action
curl "$RDEV_API_URL/projects/myapp/sdlc/next?feature=add-hello" \
-H "X-API-Key: $RDEV_API_KEY"
# Returns: {"action": "AWAIT_APPROVAL", "artifact": "spec", ...}
5. Approve Spec
curl -X POST "$RDEV_API_URL/projects/myapp/sdlc/features/add-hello/artifacts/spec/approve" \
-H "X-API-Key: $RDEV_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
6. Continue Through Phases
Repeat the pattern: Generate artifact → Check classifier → Approve → Next phase
# Generate design
curl -X POST "$RDEV_API_URL/projects/myapp/builds" \
-H "X-API-Key: $RDEV_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "/design-feature add-hello", "auto_commit": true, "auto_push": true, "git_clone_url": "https://git.threesix.ai/jordan/myapp.git"}'
# Approve design
curl -X POST "$RDEV_API_URL/projects/myapp/sdlc/features/add-hello/artifacts/design/approve" \
-H "X-API-Key: $RDEV_API_KEY" -H "Content-Type: application/json" -d '{}'
# Generate tasks
curl -X POST "$RDEV_API_URL/projects/myapp/builds" \
-H "X-API-Key: $RDEV_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "/breakdown-feature add-hello", "auto_commit": true, "auto_push": true, "git_clone_url": "https://git.threesix.ai/jordan/myapp.git"}'
# Approve tasks
curl -X POST "$RDEV_API_URL/projects/myapp/sdlc/features/add-hello/artifacts/tasks/approve" \
-H "X-API-Key: $RDEV_API_KEY" -H "Content-Type: application/json" -d '{}'
# Implement first task
curl -X POST "$RDEV_API_URL/projects/myapp/builds" \
-H "X-API-Key: $RDEV_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "/implement-task add-hello T1", "auto_commit": true, "auto_push": true, "git_clone_url": "https://git.threesix.ai/jordan/myapp.git"}'
E2E Test Script
Run the full workflow automatically:
./cookbooks/scripts/feature-dev-test.sh run my-test-project
./cookbooks/scripts/feature-dev-test.sh status my-test-project
./cookbooks/scripts/feature-dev-test.sh teardown my-test-project
Skeleton Commands Reference
These commands are installed in every project's .claude/commands/ directory and can be invoked via the /builds endpoint.
| Command | Description | Artifacts |
|---|---|---|
/spec-feature <slug> |
Create specification document | .sdlc/features/<slug>/spec.md |
/design-feature <slug> |
Create technical design | .sdlc/features/<slug>/design.md |
/breakdown-feature <slug> |
Break feature into tasks | .sdlc/features/<slug>/tasks.md |
/implement-task <slug> <task-id> |
Implement a specific task | Code changes |
/create-qa-plan <slug> |
Create QA test plan | .sdlc/features/<slug>/qa-plan.md |
/run-qa <slug> |
Execute QA tests | Test results |
/review-feature <slug> |
Code review all changes | Review report |
/audit-feature <slug> |
Audit feature for compliance | Audit report |
/fix-review-issues <slug> |
Fix issues from review | Code changes |
/remediate-audit <slug> |
Fix audit findings | Code changes |
/fix-qa-failures <slug> |
Fix failing QA tests | Code changes |
/merge-feature <slug> |
Merge feature to main | Merged branch |
/archive-feature <slug> |
Archive completed feature | Archived state |
/next <slug> |
Get classifier recommendation | Next action |
/deliver <slug> |
Run full delivery pipeline | All artifacts |
SDLC API Reference
Feature Management
| Endpoint | Method | Description |
|---|---|---|
/projects/{id}/sdlc/state |
GET | Get SDLC state for project |
/projects/{id}/sdlc/features |
GET | List all features |
/projects/{id}/sdlc/features |
POST | Create new feature |
/projects/{id}/sdlc/features/{slug} |
GET | Get feature details |
/projects/{id}/sdlc/features/{slug} |
DELETE | Delete feature |
Artifacts
| Endpoint | Method | Description |
|---|---|---|
/projects/{id}/sdlc/features/{slug}/artifacts |
GET | List artifact status |
/projects/{id}/sdlc/features/{slug}/artifacts/{type}/approve |
POST | Approve artifact |
Tasks
| Endpoint | Method | Description |
|---|---|---|
/projects/{id}/sdlc/features/{slug}/tasks |
GET | List tasks |
/projects/{id}/sdlc/features/{slug}/tasks |
POST | Add task |
Workflow Control
| Endpoint | Method | Description |
|---|---|---|
/projects/{id}/sdlc/features/{slug}/block |
POST | Block feature |
/projects/{id}/sdlc/features/{slug}/unblock |
POST | Unblock feature |
/projects/{id}/sdlc/next |
GET | Get classifier recommendation |
Queries
| Endpoint | Method | Description |
|---|---|---|
/projects/{id}/sdlc/query/blocked |
GET | List blocked features |
/projects/{id}/sdlc/query/ready |
GET | List ready features |
/projects/{id}/sdlc/query/needs-approval |
GET | List features awaiting approval |
Manual Development Workflow
For cases where you want direct control without the SDLC system, you can follow the traditional development workflow below.
Manual Feature Development Workflow
────────────────────────────────────
1. Planning → Define requirements, break into tasks
2. API Dev → Bind + Wrap patterns, OpenAPI annotations
3. Frontend Dev → Auth hooks, design system, API client
4. Testing → Handler tests, integration tests
5. Review → /review-code, quality checks
6. Deployment → Git push, Woodpecker CI, verify
This approach assumes you have:
- An rdev project with monorepo skeleton (created via
POST /project) - At least one API service component (
services/api) - A Next.js or React app component (
apps/dashboard)
Project Structure
Your composable monorepo should have:
my-project/
├── pkg/ # Shared Go packages
│ ├── app/ # Service bootstrapper
│ │ ├── app.go # NewApp, Run, RegisterRoutes
│ │ ├── handler.go # Wrap pattern
│ │ ├── bind.go # Bind + BindAndValidate
│ │ └── health.go # Health probes
│ ├── httperror/ # Typed HTTP errors
│ ├── httpresponse/ # JSON response envelope
│ ├── httpvalidation/ # go-playground/validator
│ └── auth/ # JWT + API key validation
├── packages/ # Shared TS packages
│ ├── ui/ # Design system components
│ ├── layout/ # DashboardShell, Sidebar
│ ├── auth/ # AuthProvider, useAuth
│ ├── logger/ # Structured logging
│ └── api-client/ # Generated TypeScript client
├── services/ # Go backend services
│ └── api/
│ ├── cmd/server/main.go
│ └── internal/api/routes.go
└── apps/ # Frontend applications
└── dashboard/
└── src/app/ # Next.js App Router
Step 1: Feature Planning
Create Feature Branch
git checkout -b feature/user-profile
Define Requirements
Example: User Profile Feature
- Backend: GET/PUT /api/users/me endpoints
- Frontend: Profile page with edit form
- Validation: Email format, name required
- Auth: Require authenticated user
Break Into Tasks
## Tasks
- [ ] Add User domain model with validation
- [ ] Implement GET /api/users/me handler
- [ ] Implement PUT /api/users/me handler
- [ ] Add OpenAPI annotations
- [ ] Create profile page component
- [ ] Add profile edit form
- [ ] Connect to API with generated client
- [ ] Write handler tests
- [ ] Run quality checks
Step 2: API Development
2.1 Add Domain Model
Create the domain model in services/api/internal/domain/:
// services/api/internal/domain/user.go
package domain
import "time"
// User represents an authenticated user's profile.
type User struct {
ID string `json:"id" db:"id"`
Email string `json:"email" db:"email"`
Name string `json:"name" db:"name"`
AvatarURL string `json:"avatar_url,omitempty" db:"avatar_url"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
2.2 Create Handler with Wrap Pattern
The Wrap pattern converts error-returning handlers to http.HandlerFunc:
// services/api/internal/api/handlers/user.go
package handlers
import (
"net/http"
"my-project/pkg/app"
"my-project/pkg/auth"
"my-project/pkg/httperror"
"my-project/pkg/httpresponse"
"my-project/services/api/internal/service"
)
type UserHandler struct {
svc *service.UserService
logger *slog.Logger
}
func NewUserHandler(svc *service.UserService, logger *slog.Logger) *UserHandler {
return &UserHandler{svc: svc, logger: logger}
}
// GetMe returns the authenticated user's profile.
// Uses app.Wrap to convert error-returning handler to http.HandlerFunc.
func (h *UserHandler) GetMe() http.HandlerFunc {
return app.Wrap(func(w http.ResponseWriter, r *http.Request) error {
// Get user from auth context
user, ok := auth.GetUser(r.Context())
if !ok {
return httperror.Unauthorized("not authenticated")
}
// Fetch full profile
profile, err := h.svc.GetByID(r.Context(), user.ID)
if err != nil {
return httperror.WrapError(err, "failed to get profile")
}
httpresponse.JSON(w, http.StatusOK, profile)
return nil
})
}
// UpdateMe updates the authenticated user's profile.
// Uses app.BindAndValidate for request parsing and validation.
func (h *UserHandler) UpdateMe() http.HandlerFunc {
type request struct {
Name string `json:"name" validate:"required,min=1,max=100"`
AvatarURL string `json:"avatar_url" validate:"omitempty,url"`
}
return app.Wrap(func(w http.ResponseWriter, r *http.Request) error {
// Get user from auth context
user, ok := auth.GetUser(r.Context())
if !ok {
return httperror.Unauthorized("not authenticated")
}
// Bind and validate request body
var req request
if err := app.BindAndValidate(r, &req); err != nil {
return err // Already an HTTPError with validation details
}
// Update profile
profile, err := h.svc.Update(r.Context(), user.ID, req.Name, req.AvatarURL)
if err != nil {
return httperror.WrapError(err, "failed to update profile")
}
httpresponse.JSON(w, http.StatusOK, profile)
return nil
})
}
2.3 Register Routes
// services/api/internal/api/routes.go
func (s *Server) RegisterRoutes() {
userHandler := handlers.NewUserHandler(s.userService, s.logger)
// Protected routes (require auth)
s.router.Group(func(r chi.Router) {
r.Use(auth.Middleware(s.authValidator))
r.Get("/api/users/me", userHandler.GetMe())
r.Put("/api/users/me", userHandler.UpdateMe())
})
}
2.4 Add OpenAPI Annotations
// services/api/cmd/server/main.go
func buildOpenAPISpec() *api.OpenAPISpec {
spec := api.NewOpenAPISpec("My Project API", "1.0.0", "/api")
// Security schemes
spec.WithBearerSecurity("bearerAuth", "JWT Bearer token")
spec.WithGlobalSecurity("bearerAuth")
// User profile endpoints
spec.Op("GET", "/users/me", "Get current user profile",
api.OpResponses(
api.OpResponse(200, "User profile", api.Ref("User")),
api.OpStandardResponses()...,
),
)
spec.OpWithBody("PUT", "/users/me", "Update current user profile",
api.Object(map[string]*api.Schema{
"name": api.String().Description("Display name"),
"avatar_url": api.String().Format("uri").Description("Avatar URL"),
}, "name"),
api.OpResponses(
api.OpResponse(200, "Updated user profile", api.Ref("User")),
api.OpStandardResponses()...,
),
)
// Schema definitions
spec.Schema("User", api.Object(map[string]*api.Schema{
"id": api.UUID(),
"email": api.Email(),
"name": api.String(),
"avatar_url": api.String().Format("uri"),
"created_at": api.DateTime(),
"updated_at": api.DateTime(),
}))
return spec
}
Step 3: Frontend Development
3.1 Generate TypeScript Client
After adding OpenAPI annotations, regenerate the client:
./scripts/generate-client.sh
This updates packages/api-client/src/schema.d.ts with typed endpoints.
3.2 Create Profile Page
// apps/dashboard/src/app/(dashboard)/profile/page.tsx
import { ProfileForm } from '@/components/profile-form';
import { Card, CardContent, CardHeader, CardTitle } from '@my-project/ui';
export default function ProfilePage() {
return (
<div className="p-6">
<Card>
<CardHeader>
<CardTitle>Profile Settings</CardTitle>
</CardHeader>
<CardContent>
<ProfileForm />
</CardContent>
</Card>
</div>
);
}
3.3 Create Profile Form with Auth Hook
// apps/dashboard/src/components/profile-form.tsx
'use client';
import { useState } from 'react';
import { useAuth } from '@my-project/auth';
import { Button, Input, Label } from '@my-project/ui';
import { updateProfile } from '@/actions/profile';
export function ProfileForm() {
const { user, refreshUser } = useAuth();
const [name, setName] = useState(user?.name ?? '');
const [avatarUrl, setAvatarUrl] = useState(user?.avatar_url ?? '');
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
if (!user) {
return <div>Loading...</div>;
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setIsSubmitting(true);
setError(null);
try {
const result = await updateProfile({ name, avatar_url: avatarUrl });
if (result.success) {
await refreshUser();
} else {
setError(result.error ?? 'Failed to update profile');
}
} catch (err) {
setError('An unexpected error occurred');
} finally {
setIsSubmitting(false);
}
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
value={user.email}
disabled
className="bg-surface-50"
/>
</div>
<div className="space-y-2">
<Label htmlFor="name">Display Name</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="avatar">Avatar URL</Label>
<Input
id="avatar"
type="url"
value={avatarUrl}
onChange={(e) => setAvatarUrl(e.target.value)}
placeholder="https://example.com/avatar.png"
/>
</div>
{error && (
<div className="text-sm text-error">{error}</div>
)}
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Saving...' : 'Save Changes'}
</Button>
</form>
);
}
3.4 Create Server Action
// apps/dashboard/src/actions/profile.ts
'use server';
import { revalidatePath } from 'next/cache';
import { cookies } from 'next/headers';
interface UpdateProfileInput {
name: string;
avatar_url?: string;
}
interface UpdateProfileResult {
success: boolean;
error?: string;
}
export async function updateProfile(input: UpdateProfileInput): Promise<UpdateProfileResult> {
const token = cookies().get('auth_token')?.value;
if (!token) {
return { success: false, error: 'Not authenticated' };
}
const response = await fetch(`${process.env.API_URL}/api/users/me`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify(input),
});
if (!response.ok) {
const error = await response.json();
return {
success: false,
error: error.error?.message ?? 'Failed to update profile',
};
}
revalidatePath('/profile');
return { success: true };
}
3.5 Add Navigation Link
// apps/dashboard/src/components/sidebar-nav.tsx
import { User, Settings, Home } from 'lucide-react';
const navItems = [
{ label: 'Dashboard', href: '/dashboard', icon: Home },
{ label: 'Profile', href: '/profile', icon: User },
{ label: 'Settings', href: '/settings', icon: Settings },
];
Step 4: Testing
4.1 Write Handler Tests
// services/api/internal/api/handlers/user_test.go
package handlers_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"my-project/pkg/auth"
"my-project/services/api/internal/api/handlers"
"my-project/services/api/internal/service"
)
func TestUserHandler_GetMe(t *testing.T) {
svc := service.NewUserService(/* mock deps */)
handler := handlers.NewUserHandler(svc, slog.Default())
t.Run("returns user profile", func(t *testing.T) {
// Create request with auth context
req := httptest.NewRequest("GET", "/api/users/me", nil)
req = req.WithContext(auth.SetUser(req.Context(), &auth.User{
ID: "user-123",
Email: "test@example.com",
}))
rr := httptest.NewRecorder()
handler.GetMe().ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("expected 200, got %d: %s", rr.Code, rr.Body.String())
}
var resp map[string]any
json.Unmarshal(rr.Body.Bytes(), &resp)
data := resp["data"].(map[string]any)
if data["id"] != "user-123" {
t.Errorf("expected user-123, got %v", data["id"])
}
})
t.Run("returns 401 without auth", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/users/me", nil)
rr := httptest.NewRecorder()
handler.GetMe().ServeHTTP(rr, req)
if rr.Code != http.StatusUnauthorized {
t.Errorf("expected 401, got %d", rr.Code)
}
})
}
func TestUserHandler_UpdateMe(t *testing.T) {
svc := service.NewUserService(/* mock deps */)
handler := handlers.NewUserHandler(svc, slog.Default())
t.Run("updates profile", func(t *testing.T) {
body := `{"name": "New Name", "avatar_url": "https://example.com/avatar.png"}`
req := httptest.NewRequest("PUT", "/api/users/me", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req = req.WithContext(auth.SetUser(req.Context(), &auth.User{
ID: "user-123",
}))
rr := httptest.NewRecorder()
handler.UpdateMe().ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("expected 200, got %d: %s", rr.Code, rr.Body.String())
}
})
t.Run("validates required name", func(t *testing.T) {
body := `{"name": ""}`
req := httptest.NewRequest("PUT", "/api/users/me", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req = req.WithContext(auth.SetUser(req.Context(), &auth.User{
ID: "user-123",
}))
rr := httptest.NewRecorder()
handler.UpdateMe().ServeHTTP(rr, req)
if rr.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", rr.Code)
}
})
}
4.2 Run Tests
# Run all tests
./scripts/quality.sh
# Run specific service tests
go test ./services/api/...
# Run with coverage
go test -cover ./services/api/...
Step 5: Review & Quality Checks
5.1 Run Code Review
Use the built-in review command:
# In Claude Code
/review-code
This checks for:
- Completeness and accuracy
- Tech debt indicators
- Maintainability issues
- Extensibility concerns
- DRY/CLEAN violations
5.2 Run Quality Checks
# Lint Go code
golangci-lint run ./...
# Lint frontend
cd apps/dashboard && npm run lint
# Type check
cd apps/dashboard && npm run typecheck
5.3 Verify OpenAPI Spec
# Start the service locally
cd services/api && make run
# Check the spec
curl http://localhost:8001/openapi.json | jq '.paths["/users/me"]'
# View docs
open http://localhost:8001/docs
Step 6: Deployment
6.1 Commit Changes
# Stage changes
git add services/api/internal/api/handlers/user.go
git add services/api/internal/api/routes.go
git add apps/dashboard/src/app/\(dashboard\)/profile/
git add apps/dashboard/src/components/profile-form.tsx
git add apps/dashboard/src/actions/profile.ts
# Commit
git commit -m "feat: add user profile feature
- Add GET/PUT /api/users/me endpoints with Wrap pattern
- Add profile page with edit form
- Add server action for profile updates
- Add handler tests"
6.2 Push and Monitor CI
# Push feature branch
git push -u origin feature/user-profile
# Monitor pipeline
./cookbooks/scripts/feature-test.sh status my-project
6.3 Create Pull Request
gh pr create --title "feat: add user profile feature" --body "
## Summary
- Added user profile endpoints (GET/PUT /api/users/me)
- Added profile page with edit form
- Integrated with auth system
## Testing
- Handler tests added
- Manual testing complete
## Checklist
- [x] Handler tests pass
- [x] Quality checks pass
- [x] OpenAPI spec updated
"
6.4 Verify Deployment
After merge to main:
# Check deployment
curl https://my-project.threesix.ai/api/users/me \
-H "Authorization: Bearer $TOKEN" | jq .
# Open the app
open https://my-project.threesix.ai/profile
Pattern Quick Reference
Wrap Pattern
app.Wrap(func(w http.ResponseWriter, r *http.Request) error {
// Return errors, they become proper HTTP responses
if err := doThing(); err != nil {
return httperror.WrapError(err, "failed to do thing")
}
httpresponse.JSON(w, http.StatusOK, result)
return nil
})
Bind Pattern
var req RequestType
if err := app.BindAndValidate(r, &req); err != nil {
return err // Validation errors become 400 with details
}
HTTPError Sentinels
httperror.BadRequest("invalid input")
httperror.NotFound("user not found")
httperror.Unauthorized("not authenticated")
httperror.Forbidden("access denied")
httperror.Conflict("already exists")
httperror.Internal("something went wrong")
httperror.Validation("validation failed")
Auth Context
user, ok := auth.GetUser(r.Context())
if !ok {
return httperror.Unauthorized("not authenticated")
}
React Auth Hook
const { user, isLoading, login, logout } = useAuth();
Design System Components
import { Button, Card, Input, Label, Badge } from '@my-project/ui';
import { DashboardShell, Sidebar, Header } from '@my-project/layout';
E2E Test Scripts
Feature Development (Claude + SDLC)
Test the complete feature development flow with Claude Code:
# Run full SDLC feature development flow
./cookbooks/scripts/feature-dev-test.sh run test-feature
# Check status
./cookbooks/scripts/feature-dev-test.sh status test-feature
# Cleanup
./cookbooks/scripts/feature-dev-test.sh teardown test-feature
Scaffold Validation
Test that project scaffolding creates correct patterns:
# Create project and verify patterns
./cookbooks/scripts/scaffold-test.sh run test-scaffold
# Verify specific patterns
./cookbooks/scripts/scaffold-test.sh verify-patterns test-scaffold
# Cleanup
./cookbooks/scripts/scaffold-test.sh teardown test-scaffold
Manual Troubleshooting
Handler returns 500 instead of proper error
Check that you're using httperror.* functions, not raw errors.New().
Validation errors not showing details
Ensure you're using app.BindAndValidate() which includes validation details.
Auth middleware not applying
Verify route is inside the r.Group() with auth.Middleware().
Generated client out of sync
Run ./scripts/generate-client.sh after OpenAPI changes.
Frontend not seeing auth user
Check that AuthProvider wraps your app in providers.tsx.
Chassis Framework
The pkg/chassis package provides a convenience facade over pkg/app:
import "my-project/pkg/chassis"
svc := chassis.New("my-service", chassis.WithDefaultPort(8080))
It re-exports: New, Wrap, WrapWithLogger, Bind, BindAndValidate, BindStrict, NewHealthHandler, PingChecker, HTTPChecker.
OpenAPI Documentation
Each service defines its spec in internal/api/spec.go:
spec := openapi.NewOpenAPISpec("My API", "1.0.0").
WithBearerSecurity("bearer", "JWT token")
spec.WithSchema("User", openapi.Object(map[string]openapi.Schema{
"id": openapi.UUID(),
"email": openapi.Email(),
}, "id", "email"))
spec.AddPath("/api/v1/users/{id}", "get", map[string]any{
"summary": "Get user",
"tags": []string{"Users"},
"parameters": []any{openapi.IDParam()},
"responses": map[string]any{
"200": openapi.OpResponse("Success", openapi.ResponseSchema(openapi.Ref("User"))),
"404": openapi.OpResponse("Not found", openapi.ErrorResponseSchema()),
},
})
application.EnableDocs(spec) // Mounts /docs (Scalar UI) and /openapi.json
Design System Components
Available from @project/ui:
| Component | Usage |
|---|---|
| Button | Primary actions, variants: default, destructive, outline, ghost |
| Card | Content containers with CardHeader, CardContent, CardFooter |
| Input, Label | Form fields |
| Badge | Status indicators, variants: success, warning, error, info |
| Dialog | Modal dialogs |
| Table | Data tables |
| Select | Dropdowns |
| Alert | Notification banners, variants: default, destructive, success, warning |
| Textarea | Multiline input |
| DropdownMenu | Context menus with items, checkboxes, radio groups |
| Sheet | Slide-in panels (side: top, right, bottom, left) |
All use CSS custom properties: var(--background), var(--accent), var(--border), etc.
SDLC Troubleshooting
Build returns "completed" but no artifacts created
The skeleton command may have failed silently. Check the build output:
curl "$RDEV_API_URL/builds/<task-id>" -H "X-API-Key: $RDEV_API_KEY" | jq '.data.result'
Common causes:
- Feature doesn't exist (create it first via
/sdlc/features) - Previous artifacts missing (spec must exist before design)
- SDLC CLI not installed in pod
Classifier returns AWAIT_ARTIFACT but artifact exists
The artifact may exist in the file system but not registered with SDLC:
# Register artifact manually
curl -X POST "$RDEV_API_URL/projects/$PROJECT/builds" \
-H "X-API-Key: $RDEV_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "sdlc artifact create <slug> <type>", "auto_commit": true, "auto_push": true, "git_clone_url": "<url>"}'
Task implementation doesn't commit changes
Check that auto_commit and auto_push are set to true, and git_clone_url is provided:
curl -X POST "$RDEV_API_URL/projects/$PROJECT/builds" \
-H "X-API-Key: $RDEV_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "/implement-task <slug> <task-id>",
"auto_commit": true,
"auto_push": true,
"git_clone_url": "https://git.threesix.ai/jordan/<project>.git"
}'
Build times out
Builds have a default timeout of 10 minutes. Complex implementations may need longer. Check the build status periodically:
# Poll status
while true; do
status=$(curl -s "$RDEV_API_URL/builds/<task-id>" -H "X-API-Key: $RDEV_API_KEY" | jq -r '.data.status // .status')
echo "Status: $status"
[[ "$status" == "completed" || "$status" == "failed" ]] && break
sleep 10
done
Feature stuck in wrong phase
Check the classifier recommendation to understand what's blocking:
curl "$RDEV_API_URL/projects/$PROJECT/sdlc/next?feature=<slug>" \
-H "X-API-Key: $RDEV_API_KEY" | jq '.'
If needed, you can manually advance the phase by approving the pending artifact.
Related
- Composable App Cookbook - Creating projects with components
- Landing Page Cookbook - Simple single-component sites
- Composable Monorepo Guide
- API Framework Guide
- SDLC Guide - Full SDLC orchestration documentation