fix(skeleton): enforce chi {param} URL syntax in agent guidance
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

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 <noreply@anthropic.com>
This commit is contained in:
jordan 2026-02-07 20:44:52 -07:00
parent 863dfd3214
commit 9085965864
5 changed files with 78 additions and 18 deletions

View File

@ -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-2-async-worker-pipeline` | Background jobs: Producer/consumer with Redis | Redis |
| `slackpath-3-realtime-chat` | WebSockets: Pub/sub broadcasting | Redis | | `slackpath-3-realtime-chat` | WebSockets: Pub/sub broadcasting | Redis |
| `slackpath-4-microservice-constellation` | Service mesh: Auth + Chat + Worker coordination | CockroachDB + 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:** **Running a slackpath:**
```bash ```bash
@ -499,7 +500,8 @@ cookbooks/
├── slackpath-1-authenticated-service.yaml ├── slackpath-1-authenticated-service.yaml
├── slackpath-2-async-worker-pipeline.yaml ├── slackpath-2-async-worker-pipeline.yaml
├── slackpath-3-realtime-chat.yaml ├── slackpath-3-realtime-chat.yaml
└── slackpath-4-microservice-constellation.yaml ├── slackpath-4-microservice-constellation.yaml
└── slackpath-5-full-lifecycle.yaml
``` ```
## Related ## Related

View File

@ -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`. - **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`. - **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. - **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 ## Quick Reference

View File

@ -82,30 +82,61 @@ List responses:
## Handler Pattern ## Handler Pattern
Handlers return `error` and are wrapped with `app.Wrap()`:
```go ```go
func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) { // Handler returns error - wrapped with app.Wrap() in routes.go
// 1. Parse request func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) error {
// 1. Parse and validate request
var req CreateUserRequest var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := app.BindAndValidate(r, &req); err != nil {
httpresponse.BadRequest(w, "invalid request body") return err // HTTPError - mapped to status code
return
} }
// 2. Validate // 2. Call service
if err := req.Validate(); err != nil {
httpresponse.ValidationError(w, err)
return
}
// 3. Call service
user, err := h.service.CreateUser(r.Context(), req.ToDomain()) user, err := h.service.CreateUser(r.Context(), req.ToDomain())
if err != nil { if err != nil {
httpresponse.HandleError(w, err) return mapDomainError(err) // Convert domain errors to HTTPErrors
return
} }
// 4. Respond // 3. Respond
httpresponse.Created(w, user) 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
// ...
} }
``` ```

View File

@ -10,13 +10,38 @@ You are a Go expert for the {{PROJECT_NAME}} monorepo. You write idiomatic, prod
## Stack ## Stack
- **Router:** chi/v5 - **Router:** chi/v5 (CRITICAL: Use `{param}` syntax for URL params, never `:param`)
- **Database:** sqlx (no GORM) - **Database:** sqlx (no GORM)
- **Logging:** slog - **Logging:** slog
- **Config:** environment variables - **Config:** environment variables
- **Architecture:** Hexagonal (ports & adapters) - **Architecture:** Hexagonal (ports & adapters)
- **Workspace:** go.work with shared pkg/ - **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 ## Patterns
### Service Structure ### Service Structure

View File

@ -29,6 +29,7 @@
- **Handler pattern:** All handlers return `error`, wrapped with `app.Wrap()`. HTTPErrors map to status codes; raw errors become 500. - **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`. - **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()`. - **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. - **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. - **Auth middleware:** Auth is opt-in. Use `auth.Middleware()` in route groups for protected endpoints.