# AGENTS.md Instructions for AI coding agents working in this repository. ## Quick Reference ```bash # Development task live # Hot-reload dev server (templ + tailwind + air) task build # Production build to bin/games task run # Build and run server # Quality task test # Run all tests: go test ./... task lint # Run linter: golangci-lint run # Single test go test -run TestName ./path/to/package # Code generation task build:templ # Compile .templ files task build:styles # Build TailwindCSS go generate ./... # Run sqlc for DB queries ``` ## Workflow Rules - **Never merge PRs without explicit user approval.** Create the PR, push changes, then wait. - Always use PRs via `tea` CLI - never push directly to main. - Write semantic commit messages focusing on "why" not "what". ## Project Structure ``` games/ ├── connect4/, snake/ # Game logic packages (pure Go) ├── features/ # Feature modules (handlers, routes, templates) │ ├── auth/ # Login/register │ ├── c4game/ # Connect 4 UI │ ├── snakegame/ # Snake UI │ ├── lobby/ # Game lobby │ └── common/ # Shared components, layouts ├── chat/ # Reusable chat room (NATS + persistence) ├── db/ # SQLite, migrations, sqlc queries ├── assets/ # Static files (embedded) └── config/, logging/, nats/, sessions/, router/ # Infrastructure ``` ## Code Style ### Imports Organize in three groups: stdlib, third-party, local. The linter enforces this. ```go import ( "context" "fmt" "net/http" "github.com/go-chi/chi/v5" "github.com/rs/zerolog/log" "github.com/ryanhamamura/games/connect4" "github.com/ryanhamamura/games/db/repository" ) ``` ### Naming Conventions | Type | Convention | Examples | |------|------------|----------| | Files | lowercase, underscores | `config_dev.go`, `handlers.go` | | HTTP handlers | `Handle` prefix | `HandleGamePage`, `HandleLogin` | | Constructors | `New` prefix | `NewStore`, `NewRoom` | | Getters | `Get` prefix | `GetPlayerID`, `GetGame` | | Setup functions | `Setup` prefix | `SetupRoutes`, `SetupLogger` | | Types | PascalCase | `Game`, `Player`, `Instance` | | Status enums | `Status` prefix | `StatusWaitingForPlayer`, `StatusInProgress` | | Session keys | `Key` prefix | `KeyPlayerID`, `KeyUserID` | ### Error Handling 1. **Wrap errors with context:** ```go return fmt.Errorf("loading game %s: %w", id, err) ``` 2. **Return (result, error) tuples:** ```go func loadGame(queries *repository.Queries, id string) (*Game, error) ``` 3. **Best-effort operations** - use nolint comment: ```go nc.Publish(subject, nil) //nolint:errcheck // best-effort notification ``` 4. **HTTP errors:** ```go http.Error(w, "game not found", http.StatusNotFound) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) ``` ### Comments - Focus on **why**, not **how**. Avoid superfluous comments. - Package comments at top of primary file: ```go // Package connect4 implements Connect 4 game logic, state management, and persistence. package connect4 ``` - Function comments for exported functions: ```go // DropPiece attempts to drop a piece in the given column. // Returns (row placed, success). func (g *Game) DropPiece(col, playerColor int) (int, bool) ``` ## Go Patterns ### Dependency Injection via Closures Handlers receive dependencies and return `http.HandlerFunc`: ```go func HandleGamePage(store *connect4.Store, sm *scs.SessionManager) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // use store, sm here } } ``` ### Mutex for Concurrent Access ```go type Store struct { games map[string]*Instance gamesMu sync.RWMutex } func (s *Store) Get(id string) (*Instance, bool) { s.gamesMu.RLock() defer s.gamesMu.RUnlock() inst, ok := s.games[id] return inst, ok } ``` ### Build Tags for Environment ```go //go:build dev //go:build !dev ``` ### Embedded Filesystems ```go //go:embed assets var assets embed.FS //go:embed migrations/*.sql var MigrationFS embed.FS ``` ### Graceful Shutdown ```go eg, egctx := errgroup.WithContext(ctx) eg.Go(func() error { return server.ListenAndServe() }) eg.Go(func() error { <-egctx.Done() return server.Shutdown(context.Background()) }) return eg.Wait() ``` ## Templ + Datastar Patterns ### SSE Connection with Disabled Cancellation Datastar cancels SSE on user interaction by default. Disable for persistent connections: ```go data-init={ fmt.Sprintf("@get('/games/%s/events',{requestCancellation:'disabled'})", g.ID) } ``` ### Prevent Script Duplication on SSE Patches Use `templ.NewOnceHandle()` for scripts in components that get patched: ```go var scriptHandle = templ.NewOnceHandle() templ MyComponent() {