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

192 lines
4.5 KiB
Markdown

# 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:
```go
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`)
```go
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/`:
```go
// 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}/`:
```go
// 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:
```go
// 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:
```go
if err != nil {
return fmt.Errorf("create project %s: %w", name, err)
}
```
Define domain errors in `internal/domain/errors.go`:
```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
```go
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