607 lines
18 KiB
Markdown
607 lines
18 KiB
Markdown
# Gin Server Composition Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Replace the standard-library ServeMux API layer with Gin while keeping explicit `http.Server` composition and configurable network settings.
|
|
|
|
**Architecture:** `internal/api.NewRouter` will build and return a `*gin.Engine`. `cmd/server/main.go` will pass that engine into `http.Server{Handler: router}` and continue using `ListenAndServe`, never `router.Run`. `pkg/config` will parse server timeout/header settings into strong types used by `http.Server`.
|
|
|
|
**Tech Stack:** Go 1.22+, Gin (`github.com/gin-gonic/gin`), standard library `net/http`, YAML config via `gopkg.in/yaml.v3`.
|
|
|
|
## Global Constraints
|
|
|
|
- Use Gin for API routing.
|
|
- Do not use `router.Run()` or `engine.Run()`.
|
|
- Set `*gin.Engine` as `http.Server.Handler` explicitly.
|
|
- Keep all existing API paths, HTTP methods, and JSON response shapes compatible.
|
|
- Add configurable `readTimeout`, `writeTimeout`, `idleTimeout`, and `maxHeaderBytes` server settings.
|
|
- Do not add auth, CORS, rate limiting, request logger middleware, WebSocket, Git, LSP, watcher streaming, or AI Agent orchestration.
|
|
- Use only one new dependency for this migration: `github.com/gin-gonic/gin`.
|
|
- Code generation/refactoring must be delegated to Codex MCP using exactly `model: "kimi/kimi-k2.7-code"`, `sandbox: "danger-full-access"`, and `approval-policy: "on-failure"`.
|
|
- Do not commit unless the user explicitly asks.
|
|
|
|
---
|
|
|
|
## File Structure
|
|
|
|
### Modify
|
|
|
|
- `go.mod` — add Gin dependency.
|
|
- `go.sum` — update transitive dependency checksums from `go mod tidy`.
|
|
- `configs/config.yaml` — add server network settings.
|
|
- `pkg/config/config.go` — parse duration/header config and environment overrides.
|
|
- `internal/api/router.go` — replace `http.ServeMux` with `gin.Engine` route registration.
|
|
- `internal/api/errors_helper.go` — convert error writer to Gin context.
|
|
- `internal/api/workspace_handler.go` — migrate handlers to `*gin.Context`.
|
|
- `internal/api/file_handler.go` — migrate handlers to `*gin.Context`.
|
|
- `internal/api/process_handler.go` — migrate handlers to `*gin.Context`.
|
|
- `internal/api/router_test.go` — keep smoke tests and add Gin/server compatibility assertions if useful.
|
|
- `cmd/server/main.go` — use configured server network parameters and keep explicit `http.Server`.
|
|
- `README.md` — update dependency/framework and config examples if needed.
|
|
|
|
### Create
|
|
|
|
- `pkg/config/config_test.go` — tests for default config, YAML duration parsing, environment overrides, and invalid config failures.
|
|
|
|
---
|
|
|
|
## Task 1: Add configurable server network settings
|
|
|
|
**Files:**
|
|
- Modify: `configs/config.yaml`
|
|
- Modify: `pkg/config/config.go`
|
|
- Create: `pkg/config/config_test.go`
|
|
|
|
**Interfaces:**
|
|
- Consumes: existing `config.Load(path string) (Config, error)`.
|
|
- Produces: `config.ServerConfig` with fields `Addr string`, `ReadTimeout time.Duration`, `WriteTimeout time.Duration`, `IdleTimeout time.Duration`, `MaxHeaderBytes int`.
|
|
|
|
- [ ] **Step 1: Write config tests first**
|
|
|
|
Create `pkg/config/config_test.go` with tests for these behaviors:
|
|
|
|
```go
|
|
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestDefaultIncludesServerNetworkSettings(t *testing.T) {
|
|
cfg := Default()
|
|
if cfg.Server.Addr != ":8080" {
|
|
t.Fatalf("Addr = %q, want :8080", cfg.Server.Addr)
|
|
}
|
|
if cfg.Server.ReadTimeout != 15*time.Second {
|
|
t.Fatalf("ReadTimeout = %v, want 15s", cfg.Server.ReadTimeout)
|
|
}
|
|
if cfg.Server.WriteTimeout != 15*time.Second {
|
|
t.Fatalf("WriteTimeout = %v, want 15s", cfg.Server.WriteTimeout)
|
|
}
|
|
if cfg.Server.IdleTimeout != 60*time.Second {
|
|
t.Fatalf("IdleTimeout = %v, want 60s", cfg.Server.IdleTimeout)
|
|
}
|
|
if cfg.Server.MaxHeaderBytes != 1048576 {
|
|
t.Fatalf("MaxHeaderBytes = %d, want 1048576", cfg.Server.MaxHeaderBytes)
|
|
}
|
|
}
|
|
|
|
func TestLoadParsesServerNetworkSettingsFromYAML(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "config.yaml")
|
|
data := []byte(`server:
|
|
addr: ":9090"
|
|
readTimeout: "2s"
|
|
writeTimeout: "3s"
|
|
idleTimeout: "4s"
|
|
maxHeaderBytes: 2048
|
|
workspace:
|
|
root: "./tmp-workspaces"
|
|
process:
|
|
opencodeCommand: "fake-opencode"
|
|
`)
|
|
if err := os.WriteFile(path, data, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load returned error: %v", err)
|
|
}
|
|
if cfg.Server.Addr != ":9090" {
|
|
t.Fatalf("Addr = %q, want :9090", cfg.Server.Addr)
|
|
}
|
|
if cfg.Server.ReadTimeout != 2*time.Second || cfg.Server.WriteTimeout != 3*time.Second || cfg.Server.IdleTimeout != 4*time.Second {
|
|
t.Fatalf("timeouts = %v/%v/%v, want 2s/3s/4s", cfg.Server.ReadTimeout, cfg.Server.WriteTimeout, cfg.Server.IdleTimeout)
|
|
}
|
|
if cfg.Server.MaxHeaderBytes != 2048 {
|
|
t.Fatalf("MaxHeaderBytes = %d, want 2048", cfg.Server.MaxHeaderBytes)
|
|
}
|
|
}
|
|
|
|
func TestLoadAppliesServerEnvironmentOverrides(t *testing.T) {
|
|
t.Setenv("CODESPACE_ADDR", ":7070")
|
|
t.Setenv("CODESPACE_READ_TIMEOUT", "5s")
|
|
t.Setenv("CODESPACE_WRITE_TIMEOUT", "6s")
|
|
t.Setenv("CODESPACE_IDLE_TIMEOUT", "7s")
|
|
t.Setenv("CODESPACE_MAX_HEADER_BYTES", "4096")
|
|
|
|
cfg, err := Load(filepath.Join(t.TempDir(), "missing.yaml"))
|
|
if err != nil {
|
|
t.Fatalf("Load returned error: %v", err)
|
|
}
|
|
if cfg.Server.Addr != ":7070" {
|
|
t.Fatalf("Addr = %q, want :7070", cfg.Server.Addr)
|
|
}
|
|
if cfg.Server.ReadTimeout != 5*time.Second || cfg.Server.WriteTimeout != 6*time.Second || cfg.Server.IdleTimeout != 7*time.Second {
|
|
t.Fatalf("timeouts = %v/%v/%v, want 5s/6s/7s", cfg.Server.ReadTimeout, cfg.Server.WriteTimeout, cfg.Server.IdleTimeout)
|
|
}
|
|
if cfg.Server.MaxHeaderBytes != 4096 {
|
|
t.Fatalf("MaxHeaderBytes = %d, want 4096", cfg.Server.MaxHeaderBytes)
|
|
}
|
|
}
|
|
|
|
func TestLoadRejectsInvalidDuration(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "config.yaml")
|
|
data := []byte(`server:
|
|
readTimeout: "garbage"
|
|
`)
|
|
if err := os.WriteFile(path, data, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Load(path); err == nil {
|
|
t.Fatal("expected invalid duration error")
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Run tests to verify failure**
|
|
|
|
Run:
|
|
|
|
```sh
|
|
go test ./pkg/config
|
|
```
|
|
|
|
Expected:
|
|
|
|
- Fails because `ServerConfig` lacks timeout/header fields.
|
|
|
|
- [ ] **Step 3: Implement config parsing**
|
|
|
|
Modify `pkg/config/config.go`:
|
|
|
|
- Import `fmt`, `strconv`, and `time`.
|
|
- Change public `ServerConfig` to:
|
|
|
|
```go
|
|
type ServerConfig struct {
|
|
Addr string
|
|
ReadTimeout time.Duration
|
|
WriteTimeout time.Duration
|
|
IdleTimeout time.Duration
|
|
MaxHeaderBytes int
|
|
}
|
|
```
|
|
|
|
- Introduce raw YAML structs:
|
|
|
|
```go
|
|
type rawConfig struct {
|
|
Server rawServerConfig `yaml:"server"`
|
|
Workspace WorkspaceConfig `yaml:"workspace"`
|
|
Process ProcessConfig `yaml:"process"`
|
|
}
|
|
|
|
type rawServerConfig struct {
|
|
Addr string `yaml:"addr"`
|
|
ReadTimeout string `yaml:"readTimeout"`
|
|
WriteTimeout string `yaml:"writeTimeout"`
|
|
IdleTimeout string `yaml:"idleTimeout"`
|
|
MaxHeaderBytes int `yaml:"maxHeaderBytes"`
|
|
}
|
|
```
|
|
|
|
- `Default()` returns `15*time.Second`, `15*time.Second`, `60*time.Second`, and `1048576`.
|
|
- `Load(path)` starts from `Default()`, unmarshals YAML into `rawConfig`, applies only non-zero/non-empty raw fields, parses durations using `time.ParseDuration`, then applies env.
|
|
- `applyEnv` parses:
|
|
- `CODESPACE_READ_TIMEOUT`
|
|
- `CODESPACE_WRITE_TIMEOUT`
|
|
- `CODESPACE_IDLE_TIMEOUT`
|
|
- `CODESPACE_MAX_HEADER_BYTES`
|
|
- Invalid duration or integer env values return an error from `Load`, not silent fallback.
|
|
|
|
A clean implementation shape:
|
|
|
|
```go
|
|
func applyRaw(cfg *Config, raw rawConfig) error
|
|
func applyDuration(target *time.Duration, value string, name string) error
|
|
func applyEnv(cfg *Config) error
|
|
```
|
|
|
|
- [ ] **Step 4: Update YAML config**
|
|
|
|
Modify `configs/config.yaml` to:
|
|
|
|
```yaml
|
|
server:
|
|
addr: ":8080"
|
|
readTimeout: "15s"
|
|
writeTimeout: "15s"
|
|
idleTimeout: "60s"
|
|
maxHeaderBytes: 1048576
|
|
workspace:
|
|
root: "./workspaces"
|
|
process:
|
|
opencodeCommand: "opencode"
|
|
```
|
|
|
|
- [ ] **Step 5: Verify config package**
|
|
|
|
Run:
|
|
|
|
```sh
|
|
gofmt -w pkg/config
|
|
go test ./pkg/config
|
|
```
|
|
|
|
Expected:
|
|
|
|
- Tests pass.
|
|
|
|
---
|
|
|
|
## Task 2: Migrate API router and handlers to Gin
|
|
|
|
**Files:**
|
|
- Modify: `go.mod`
|
|
- Modify: `go.sum`
|
|
- Modify: `internal/api/router.go`
|
|
- Modify: `internal/api/errors_helper.go`
|
|
- Modify: `internal/api/workspace_handler.go`
|
|
- Modify: `internal/api/file_handler.go`
|
|
- Modify: `internal/api/process_handler.go`
|
|
- Modify: `internal/api/router_test.go`
|
|
|
|
**Interfaces:**
|
|
- Consumes: service constructors and methods unchanged.
|
|
- Produces: `api.NewRouter(workspaces *service.WorkspaceService, files *service.FileService, processes *service.ProcessService) *gin.Engine`.
|
|
|
|
- [ ] **Step 1: Add Gin dependency**
|
|
|
|
Run:
|
|
|
|
```sh
|
|
go get github.com/gin-gonic/gin
|
|
```
|
|
|
|
Expected:
|
|
|
|
- `go.mod` contains `github.com/gin-gonic/gin`.
|
|
- `go.sum` is updated.
|
|
|
|
- [ ] **Step 2: Keep HTTP tests as compatibility tests**
|
|
|
|
`internal/api/router_test.go` can keep using `http.Handler` because `*gin.Engine` implements `ServeHTTP`. Update setup only if compile requires it:
|
|
|
|
```go
|
|
func setupTestRouter(t *testing.T) http.Handler {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
wsMgr := workspace.NewLocalManager(root)
|
|
procMgr := process.NewManager("")
|
|
wsSvc := service.NewWorkspaceService(wsMgr, procMgr)
|
|
fileSvc := service.NewFileService(wsMgr)
|
|
procSvc := service.NewProcessService(wsMgr, procMgr)
|
|
return NewRouter(wsSvc, fileSvc, procSvc)
|
|
}
|
|
```
|
|
|
|
Existing tests must continue to assert the same HTTP status codes and JSON response bodies.
|
|
|
|
- [ ] **Step 3: Implement Gin router**
|
|
|
|
Modify `internal/api/router.go` to:
|
|
|
|
```go
|
|
package api
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"codespace/internal/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func NewRouter(workspaces *service.WorkspaceService, files *service.FileService, processes *service.ProcessService) *gin.Engine {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
r := gin.New()
|
|
r.Use(gin.Recovery())
|
|
|
|
wsHandler := &workspaceHandler{svc: workspaces}
|
|
fileHandler := &fileHandler{svc: files}
|
|
procHandler := &processHandler{svc: processes}
|
|
|
|
r.GET("/healthz", healthHandler)
|
|
|
|
api := r.Group("/api")
|
|
api.POST("/workspaces", wsHandler.create)
|
|
api.GET("/workspaces/:id", wsHandler.get)
|
|
api.DELETE("/workspaces/:id", wsHandler.delete)
|
|
|
|
api.GET("/workspaces/:id/files", fileHandler.list)
|
|
api.GET("/workspaces/:id/files/read", fileHandler.read)
|
|
api.PUT("/workspaces/:id/files/write", fileHandler.write)
|
|
api.POST("/workspaces/:id/files/mkdir", fileHandler.mkdir)
|
|
api.DELETE("/workspaces/:id/files", fileHandler.remove)
|
|
api.POST("/workspaces/:id/files/rename", fileHandler.rename)
|
|
|
|
api.POST("/workspaces/:id/process/start", procHandler.start)
|
|
api.POST("/workspaces/:id/process/stop", procHandler.stop)
|
|
api.POST("/workspaces/:id/process/restart", procHandler.restart)
|
|
api.GET("/workspaces/:id/process/status", procHandler.status)
|
|
|
|
return r
|
|
}
|
|
|
|
func healthHandler(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Update error helper for Gin**
|
|
|
|
Modify `internal/api/errors_helper.go` to use:
|
|
|
|
```go
|
|
func writeError(c *gin.Context, err error)
|
|
```
|
|
|
|
Required behavior:
|
|
|
|
- Use `util.CodeOf(err)`.
|
|
- Map bad request/not found/conflict/internal to 400/404/409/500.
|
|
- Respond with `response.ErrorBody{Error: response.ErrorDetail{Code: string(code), Message: message}}`.
|
|
- If `err == nil`, message should be `http.StatusText(status)`.
|
|
|
|
- [ ] **Step 5: Update workspace handler**
|
|
|
|
Modify `internal/api/workspace_handler.go`:
|
|
|
|
- Remove `encoding/json` import.
|
|
- Handler signatures use `*gin.Context`.
|
|
- `create` uses `c.ShouldBindJSON(&req)`.
|
|
- `get/delete` use `c.Param("id")`.
|
|
- Success responses use `c.JSON(...)` or `c.Status(http.StatusNoContent)`.
|
|
|
|
- [ ] **Step 6: Update file handler**
|
|
|
|
Modify `internal/api/file_handler.go`:
|
|
|
|
- Remove `encoding/json` import.
|
|
- Handler signatures use `*gin.Context`.
|
|
- Workspace ID uses `c.Param("id")`.
|
|
- Query path uses `c.Query("path")`.
|
|
- JSON body uses `c.ShouldBindJSON(&req)`.
|
|
- No-content responses use `c.Status(http.StatusNoContent)`.
|
|
|
|
- [ ] **Step 7: Update process handler**
|
|
|
|
Modify `internal/api/process_handler.go`:
|
|
|
|
- Handler signatures use `*gin.Context`.
|
|
- Workspace ID uses `c.Param("id")`.
|
|
- Success responses use `c.Status(http.StatusNoContent)` or `c.JSON(...)`.
|
|
|
|
- [ ] **Step 8: Verify API package**
|
|
|
|
Run:
|
|
|
|
```sh
|
|
gofmt -w internal/api
|
|
go test ./internal/api
|
|
```
|
|
|
|
Expected:
|
|
|
|
- API tests pass with the same status/body assertions.
|
|
|
|
---
|
|
|
|
## Task 3: Wire Gin engine into explicit http.Server
|
|
|
|
**Files:**
|
|
- Modify: `cmd/server/main.go`
|
|
- Modify: `README.md`
|
|
|
|
**Interfaces:**
|
|
- Consumes: `api.NewRouter(...) *gin.Engine`.
|
|
- Consumes: `cfg.Server.ReadTimeout`, `cfg.Server.WriteTimeout`, `cfg.Server.IdleTimeout`, `cfg.Server.MaxHeaderBytes`.
|
|
- Produces: explicit `http.Server{Handler: router}` startup without Gin `Run()`.
|
|
|
|
- [ ] **Step 1: Update server wiring**
|
|
|
|
Modify `cmd/server/main.go` server construction to:
|
|
|
|
```go
|
|
router := api.NewRouter(workspaceSvc, fileSvc, processSvc)
|
|
|
|
server := &http.Server{
|
|
Addr: cfg.Server.Addr,
|
|
Handler: router,
|
|
ReadTimeout: cfg.Server.ReadTimeout,
|
|
WriteTimeout: cfg.Server.WriteTimeout,
|
|
IdleTimeout: cfg.Server.IdleTimeout,
|
|
MaxHeaderBytes: cfg.Server.MaxHeaderBytes,
|
|
}
|
|
```
|
|
|
|
Remove direct `time` usage if it becomes unused except shutdown timeout. Keep `10*time.Second` for graceful shutdown context unless you add a separate config field; do not add that field in this task.
|
|
|
|
- [ ] **Step 2: Update README**
|
|
|
|
Update `README.md` so it says:
|
|
|
|
- The API router uses Gin.
|
|
- The server is still started through explicit `http.Server`, not `gin.Engine.Run()`.
|
|
- Config supports `server.readTimeout`, `server.writeTimeout`, `server.idleTimeout`, and `server.maxHeaderBytes`.
|
|
|
|
Do not add promises for auth/CORS/Git/LSP/frontend.
|
|
|
|
- [ ] **Step 3: Verify server package**
|
|
|
|
Run:
|
|
|
|
```sh
|
|
gofmt -w cmd/server
|
|
go test ./cmd/server ./pkg/config ./internal/api
|
|
```
|
|
|
|
Expected:
|
|
|
|
- All listed packages pass.
|
|
|
|
---
|
|
|
|
## Task 4: Final validation and safety checks
|
|
|
|
**Files:**
|
|
- Modify only files from Tasks 1-3 if validation exposes issues.
|
|
|
|
**Interfaces:**
|
|
- Produces validated Gin migration.
|
|
|
|
- [ ] **Step 1: Format, tidy, and test everything**
|
|
|
|
Run:
|
|
|
|
```sh
|
|
gofmt -w cmd internal pkg
|
|
go mod tidy
|
|
go test ./...
|
|
```
|
|
|
|
Expected:
|
|
|
|
- All tests pass.
|
|
|
|
- [ ] **Step 2: Check Gin Run is not used**
|
|
|
|
Run:
|
|
|
|
```sh
|
|
grep -R "\.Run(" cmd internal || true
|
|
```
|
|
|
|
Expected:
|
|
|
|
- No output.
|
|
|
|
- [ ] **Step 3: Check explicit http.Server composition exists**
|
|
|
|
Run:
|
|
|
|
```sh
|
|
grep -R "http.Server" -n cmd/server/main.go
|
|
grep -R "Handler:" -n cmd/server/main.go
|
|
grep -R "MaxHeaderBytes" -n cmd/server/main.go pkg/config configs/config.yaml
|
|
```
|
|
|
|
Expected:
|
|
|
|
- `cmd/server/main.go` contains `http.Server`, `Handler: router`, and `MaxHeaderBytes: cfg.Server.MaxHeaderBytes`.
|
|
- `pkg/config/config.go` and `configs/config.yaml` contain `MaxHeaderBytes`/`maxHeaderBytes`.
|
|
|
|
- [ ] **Step 4: Run server smoke test**
|
|
|
|
Run:
|
|
|
|
```sh
|
|
(go run ./cmd/server > /tmp/codespace-server.log 2>&1 & pid=$!; \
|
|
for i in $(seq 1 30); do \
|
|
if curl -fsS http://localhost:8080/healthz; then \
|
|
kill $pid; wait $pid 2>/dev/null || true; exit 0; \
|
|
fi; \
|
|
sleep 0.2; \
|
|
done; \
|
|
kill $pid 2>/dev/null || true; \
|
|
wait $pid 2>/dev/null || true; \
|
|
cat /tmp/codespace-server.log; exit 1)
|
|
```
|
|
|
|
Expected:
|
|
|
|
```json
|
|
{"status":"ok"}
|
|
```
|
|
|
|
- [ ] **Step 5: Check git diff for scope creep**
|
|
|
|
Run:
|
|
|
|
```sh
|
|
git diff --stat
|
|
git status --short
|
|
```
|
|
|
|
Expected:
|
|
|
|
- Changes are limited to Gin migration, config, docs/spec/plan, `.gitignore`, and previously generated skeleton files.
|
|
- No auth/CORS/Git/LSP/frontend implementation appears.
|
|
|
|
---
|
|
|
|
## Codex Execution Prompt
|
|
|
|
Use this exact structure when dispatching Codex:
|
|
|
|
```text
|
|
## Context
|
|
- Project root: /Users/taochen/llm/codespace
|
|
- Current branch: feat/codespace-backend-skeleton
|
|
- Existing backend is a Go 1.22 codespace service.
|
|
- Current router uses standard library http.ServeMux in internal/api/router.go.
|
|
- Server already uses explicit http.Server in cmd/server/main.go.
|
|
- Design spec: docs/superpowers/specs/2026-07-02-gin-server-composition-design.md
|
|
- Implementation plan: docs/superpowers/plans/2026-07-02-gin-server-composition.md
|
|
|
|
## Task
|
|
Migrate routing to Gin and configure explicit http.Server network settings.
|
|
Steps:
|
|
1. Add Gin dependency.
|
|
2. Add configurable server read/write/idle timeout and maxHeaderBytes settings.
|
|
3. Replace internal/api ServeMux router and handlers with Gin equivalents.
|
|
4. Keep cmd/server/main.go using explicit http.Server{Handler: router}; do not use router.Run().
|
|
5. Update tests and README.
|
|
6. Run final validation.
|
|
|
|
## Constraints
|
|
- Do not change API paths, methods, or response JSON shapes.
|
|
- Do not change service interfaces.
|
|
- Do not add auth, CORS, logging middleware, rate limiting, WebSocket, Git, LSP, frontend, watcher streaming, or AI Agent features.
|
|
- Use only one new dependency: github.com/gin-gonic/gin.
|
|
- Do not commit.
|
|
|
|
## Acceptance
|
|
- gofmt -w cmd internal pkg succeeds.
|
|
- go mod tidy succeeds.
|
|
- go test ./... passes.
|
|
- grep -R "\.Run(" cmd internal || true returns no output.
|
|
- cmd/server/main.go contains explicit http.Server with Handler: router and MaxHeaderBytes from config.
|
|
- go run ./cmd/server starts and /healthz returns {"status":"ok"}.
|
|
```
|
|
|
|
---
|
|
|
|
## Self-Review
|
|
|
|
- Spec coverage: Plan covers Gin routing, handler migration, explicit server composition, configurable network settings, docs, tests, and validation against `Run()` misuse.
|
|
- Placeholder scan: No TBD/TODO/ambiguous implementation steps remain.
|
|
- Type consistency: `api.NewRouter` consistently returns `*gin.Engine`; `cmd/server/main.go` consumes it as `http.Server.Handler`; `ServerConfig` fields are consistent across plan tasks.
|
|
- Scope check: Plan is limited to Gin migration and server config. It does not introduce auth, CORS, WebSocket, Git, LSP, frontend, watcher streaming, or Agent features.
|