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>
4.5 KiB
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:
PascalCaseorSCREAMING_SNAKEfor 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