rdev/CODING_GUIDELINES.md
jordan 39df51defd feat: Add multi-provider code agent interface with Claude Code and OpenCode adapters
Implements weeks 1-4 of the multi-provider architecture:

Week 1 - Foundation:
- Add domain models (AgentProvider, AgentRequest, AgentEvent, AgentResult)
- Define CodeAgent port interface with Execute, Cancel, Capabilities
- Create thread-safe provider registry with first-registered default

Week 2 - Claude Code Adapter:
- Extract kubectl exec logic into CodeAgent implementation
- Parse stream-json output format (init, message, tool_use, result)
- Support session continuation via --resume flag

Week 3 - OpenCode Adapter:
- HTTP/SSE client for opencode serve API
- Session management (create, send message, abort)
- Event streaming with documented buffer rationale

Week 4 - Quality & Polish:
- Fix race condition in OpenCode Cancel method
- Add AgentRequest.Validate() with ErrPromptRequired, ErrInvalidTimeout
- Document DefaultAvailabilityTimeout constants
- Add HTTP error context for debugging

Also includes:
- Work queue system with PostgreSQL adapter
- Credential store for infrastructure secrets
- Project templates with Woodpecker CI integration
- Comprehensive test coverage

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 09:25:51 -07:00

4.5 KiB

Coding Guidelines

Tech Stack

Component Required Forbidden
HTTP Router chi/v5 gin, echo, fiber
Database sqlx GORM, raw sql
Logging slog log, logrus, zap
Config Viper + env vars os.Getenv directly
Testing testing + testify ginkgo, gomega
Kubernetes client-go kubectl subprocess

File Structure

cmd/{service}/         # Entry points
internal/              # Private code (hexagonal)
├── domain/            # Pure models, no deps
├── port/              # Interfaces only
├── service/           # Business logic
├── handlers/          # HTTP handlers
├── adapter/           # Infrastructure
├── auth/              # Authentication
├── middleware/        # HTTP middleware
├── worker/            # Background jobs
└── webhook/           # Event dispatch
pkg/                   # Public packages

Handler Pattern

All handlers implement the Mount interface and use pkg/api for responses:

type UsersHandler struct {
    service *service.UserService
}

func (h *UsersHandler) Mount(r chi.Router) {
    r.Route("/users", func(r chi.Router) {
        r.Get("/", h.List)
        r.Post("/", h.Create)
        r.Get("/{id}", h.Get)
    })
}

func (h *UsersHandler) Create(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        api.BadRequest(w, "invalid request body")
        return
    }

    user, err := h.service.Create(r.Context(), req)
    if err != nil {
        api.Error(w, err)
        return
    }

    api.Created(w, user)
}

Response Helpers (pkg/api)

api.OK(w, data)           // 200 with JSON body
api.Created(w, data)      // 201 with JSON body
api.NoContent(w)          // 204
api.BadRequest(w, msg)    // 400
api.Unauthorized(w, msg)  // 401
api.Forbidden(w, msg)     // 403
api.NotFound(w, msg)      // 404
api.Conflict(w, msg)      // 409
api.Error(w, err)         // 500 (or mapped status)

Port/Adapter Pattern

Domain interfaces in internal/port/:

// port/project.go
type ProjectRepository interface {
    List(ctx context.Context) ([]domain.Project, error)
    Get(ctx context.Context, id string) (*domain.Project, error)
}

type CommandExecutor interface {
    Execute(ctx context.Context, project string, cmd domain.Command) (*domain.CommandResult, error)
}

Implementations in internal/adapter/{name}/:

// adapter/kubernetes/repository.go
type Repository struct {
    client kubernetes.Interface
}

func (r *Repository) List(ctx context.Context) ([]domain.Project, error) {
    // Implementation using client-go
}

Domain Models

Pure structs in internal/domain/, NO external dependencies:

// domain/project.go
package domain

type Project struct {
    ID        string
    Name      string
    Status    ProjectStatus
    Workspace string
}

type ProjectStatus string

const (
    StatusRunning  ProjectStatus = "running"
    StatusPending  ProjectStatus = "pending"
    StatusFailed   ProjectStatus = "failed"
)

Error Handling

Use wrapped errors with context:

if err != nil {
    return fmt.Errorf("create project %s: %w", name, err)
}

Define domain errors in internal/domain/errors.go:

var (
    ErrNotFound     = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
    ErrConflict     = errors.New("already exists")
)

Testing

  • Unit tests next to source: foo_test.go
  • Table-driven tests preferred
  • Mocks implement port interfaces
  • Test files in testutil/ for shared helpers
func TestHandler_Create(t *testing.T) {
    tests := []struct {
        name       string
        input      CreateRequest
        wantStatus int
    }{
        {"valid", CreateRequest{Name: "test"}, 201},
        {"empty name", CreateRequest{}, 400},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // ...
        })
    }
}

Naming Conventions

  • Files: snake_case.go
  • Packages: lowercase (no underscores)
  • Types: PascalCase
  • Functions/Methods: PascalCase (exported), camelCase (private)
  • Constants: PascalCase or SCREAMING_SNAKE for env vars
  • Interfaces: Don't prefix with I, suffix with role (Repository, Service)

Size Limits

  • 500 lines max per file - split when exceeded
  • 100 lines max per function - extract helpers
  • 5 parameters max per function - use struct if more