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>
192 lines
4.5 KiB
Markdown
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
|