rdev/cookbooks/feature-development.md
jordan 64ccf0b85d feat: add feature development E2E test and SDLC handler fixes
- 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>
2026-02-02 20:12:40 -07:00

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:

  1. SDLC-Driven (Recommended) - Structured workflow using the SDLC system and Claude Code
  2. 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 artifactCheck classifierApproveNext 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 };
}
// 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.