From 9085965864ca5a80f7e5825b5ebc59db988c3c38 Mon Sep 17 00:00:00 2001 From: jordan Date: Sat, 7 Feb 2026 20:44:52 -0700 Subject: [PATCH] fix(skeleton): enforce chi {param} URL syntax in agent guidance Agents were generating `:id` (Echo/Gin style) instead of `{id}` (chi style), causing routes to not match. Updated api-designer, go-specialist agents and skeleton CLAUDE.md with explicit CRITICAL notes about brace syntax. Co-Authored-By: Claude Opus 4.5 --- .claude/guides/services/cookbook-trees.md | 4 +- CLAUDE.md | 1 + .../skeleton/.claude/agents/api-designer.md | 63 ++++++++++++++----- .../skeleton/.claude/agents/go-specialist.md | 27 +++++++- .../templates/skeleton/CLAUDE.md.tmpl | 1 + 5 files changed, 78 insertions(+), 18 deletions(-) diff --git a/.claude/guides/services/cookbook-trees.md b/.claude/guides/services/cookbook-trees.md index bbe9fdf..d2b06f2 100644 --- a/.claude/guides/services/cookbook-trees.md +++ b/.claude/guides/services/cookbook-trees.md @@ -465,6 +465,7 @@ Progressive complexity paths for building Slack-like platforms: | `slackpath-2-async-worker-pipeline` | Background jobs: Producer/consumer with Redis | Redis | | `slackpath-3-realtime-chat` | WebSockets: Pub/sub broadcasting | Redis | | `slackpath-4-microservice-constellation` | Service mesh: Auth + Chat + Worker coordination | CockroachDB + Redis | +| `slackpath-5-full-lifecycle` | Full SDLC: All 10 phases with explicit artifact approvals | CockroachDB | **Running a slackpath:** ```bash @@ -499,7 +500,8 @@ cookbooks/ ├── slackpath-1-authenticated-service.yaml ├── slackpath-2-async-worker-pipeline.yaml ├── slackpath-3-realtime-chat.yaml - └── slackpath-4-microservice-constellation.yaml + ├── slackpath-4-microservice-constellation.yaml + └── slackpath-5-full-lifecycle.yaml ``` ## Related diff --git a/CLAUDE.md b/CLAUDE.md index edefe61..edebd3b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -76,6 +76,7 @@ When discussing code: "add to **platform**" = edit rdev; "add to **skeleton**" = - **Validation:** Use `validate.New()` accumulator for 2+ field checks in handlers: `v := validate.New(); v.Required(req.Name, "name"); v.Required(req.Type, "type"); if err := v.Error() { ... }`. Single-field checks can stay inline. NEVER duplicate validation logic that exists in `internal/validate`. - **Error wrapping:** ALWAYS use `%w` (not `%v`) when wrapping errors in `fmt.Errorf`. Using `%v` stringifies the error and breaks `errors.Is`/`errors.As` chains. For non-error types (structs, slices), create a typed error implementing `error` instead of stringifying with `%v`. - **Context propagation:** NEVER use `context.Background()` in handlers, services, or adapters that receive a context parameter. Always derive from parent context. Use `context.WithoutCancel(ctx)` for fire-and-forget goroutines that need tracing but independent cancellation. +- **Cookbooks:** Load `.claude/skills/cookbook-scripts/SKILL.md` before writing/modifying any cookbook script or tree. ## Quick Reference diff --git a/internal/adapter/templates/templates/skeleton/.claude/agents/api-designer.md b/internal/adapter/templates/templates/skeleton/.claude/agents/api-designer.md index 4f76b7f..1e9ab89 100644 --- a/internal/adapter/templates/templates/skeleton/.claude/agents/api-designer.md +++ b/internal/adapter/templates/templates/skeleton/.claude/agents/api-designer.md @@ -82,30 +82,61 @@ List responses: ## Handler Pattern +Handlers return `error` and are wrapped with `app.Wrap()`: + ```go -func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) { - // 1. Parse request +// Handler returns error - wrapped with app.Wrap() in routes.go +func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) error { + // 1. Parse and validate request var req CreateUserRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httpresponse.BadRequest(w, "invalid request body") - return + if err := app.BindAndValidate(r, &req); err != nil { + return err // HTTPError - mapped to status code } - // 2. Validate - if err := req.Validate(); err != nil { - httpresponse.ValidationError(w, err) - return - } - - // 3. Call service + // 2. Call service user, err := h.service.CreateUser(r.Context(), req.ToDomain()) if err != nil { - httpresponse.HandleError(w, err) - return + return mapDomainError(err) // Convert domain errors to HTTPErrors } - // 4. Respond - httpresponse.Created(w, user) + // 3. Respond + httpresponse.Created(w, r, user) + return nil +} +``` + +Route registration with `app.Wrap()`: + +```go +// routes.go +application.Route("/api/users", func(r chi.Router) { + r.Get("/", app.Wrap(h.List)) + r.Post("/", app.Wrap(h.Create)) + r.Get("/{id}", app.Wrap(h.Get)) // Use {id} for chi URL params + r.Put("/{id}", app.Wrap(h.Update)) + r.Delete("/{id}", app.Wrap(h.Delete)) +}) +``` + +## Chi URL Parameters + +**CRITICAL:** Use brace syntax `{param}` for URL parameters. chi does NOT support colon syntax `:param`. + +```go +// CORRECT - chi uses braces +r.Get("/users/{id}", handler) // chi.URLParam(r, "id") +r.Get("/users/{id}/posts/{postID}", handler) // chi.URLParam(r, "postID") + +// WRONG - will not work with chi +r.Get("/users/:id", handler) // This registers literal "/:id" path! +``` + +Extracting URL parameters: + +```go +func (h *Handler) Get(w http.ResponseWriter, r *http.Request) error { + id := chi.URLParam(r, "id") // Extract from {id} in route pattern + // ... } ``` diff --git a/internal/adapter/templates/templates/skeleton/.claude/agents/go-specialist.md b/internal/adapter/templates/templates/skeleton/.claude/agents/go-specialist.md index 441f14a..bc0bcfb 100644 --- a/internal/adapter/templates/templates/skeleton/.claude/agents/go-specialist.md +++ b/internal/adapter/templates/templates/skeleton/.claude/agents/go-specialist.md @@ -10,13 +10,38 @@ You are a Go expert for the {{PROJECT_NAME}} monorepo. You write idiomatic, prod ## Stack -- **Router:** chi/v5 +- **Router:** chi/v5 (CRITICAL: Use `{param}` syntax for URL params, never `:param`) - **Database:** sqlx (no GORM) - **Logging:** slog - **Config:** environment variables - **Architecture:** Hexagonal (ports & adapters) - **Workspace:** go.work with shared pkg/ +## Chi Router Patterns + +```go +// Route registration +application.Route("/api/users", func(r chi.Router) { + r.Get("/", handler.List) + r.Post("/", handler.Create) + r.Get("/{id}", handler.Get) // Use {id}, never :id + r.Put("/{id}", handler.Update) + r.Delete("/{id}", handler.Delete) + + // Protected routes with middleware + r.Group(func(r chi.Router) { + r.Use(auth.Middleware(...)) + r.Post("/{id}/admin", handler.Admin) + }) +}) + +// Extract URL params with chi.URLParam +func (h *Handler) Get(w http.ResponseWriter, r *http.Request) error { + id := chi.URLParam(r, "id") + // ... +} +``` + ## Patterns ### Service Structure diff --git a/internal/adapter/templates/templates/skeleton/CLAUDE.md.tmpl b/internal/adapter/templates/templates/skeleton/CLAUDE.md.tmpl index 436e09c..f7b550e 100644 --- a/internal/adapter/templates/templates/skeleton/CLAUDE.md.tmpl +++ b/internal/adapter/templates/templates/skeleton/CLAUDE.md.tmpl @@ -29,6 +29,7 @@ - **Handler pattern:** All handlers return `error`, wrapped with `app.Wrap()`. HTTPErrors map to status codes; raw errors become 500. - **Request binding:** Always use `app.Bind()` or `app.BindAndValidate()`. Never use raw `json.NewDecoder`. +- **URL parameters:** Use brace syntax `{param}` for chi URL parameters. NEVER use colon syntax `:param` - it won't work and will cause 404s. Extract with `chi.URLParam(r, "param")`. - **Error types:** Use `httperror.BadRequest`, `httperror.NotFound`, etc. Never bare `http.Error()`. - **Response envelope:** Use `httpresponse.OK`, `httpresponse.Created`, `httpresponse.NoContent`. All responses use `{data, meta}` envelope. - **Auth middleware:** Auth is opt-in. Use `auth.Middleware()` in route groups for protected endpoints.