// atomizer-mock is the standalone placeholder Atomization Service (§5.1, // §9.1). It calls Anthropic through the OpenAI-compatible chat-completions // endpoint by default, or the native Messages API when // LLM_API_STYLE=anthropic, and falls back to a deterministic equal split // when no ANTHROPIC_API_KEY is configured so the whole system works offline. package main import ( "context" "encoding/json" "fmt" "log/slog" "net/http" "os" "os/signal" "strings" "syscall" "time" ) type config struct { port string token string apiKey string baseURL string model string apiStyle string // openai | anthropic } func loadConfig() config { get := func(k, def string) string { if v := os.Getenv(k); v != "" { return v } return def } return config{ port: get("PORT", "8090"), token: os.Getenv("ATOMIZER_TOKEN"), apiKey: os.Getenv("ANTHROPIC_API_KEY"), baseURL: strings.TrimRight(get("ANTHROPIC_OPENAI_BASE_URL", "https://api.anthropic.com/v1"), "/"), model: get("ANTHROPIC_MODEL", "claude-sonnet-4-6"), apiStyle: get("LLM_API_STYLE", "openai"), } } type subTask struct { Title string `json:"title"` Description string `json:"description"` AcceptanceCriteria []string `json:"acceptanceCriteria"` EffortCoefficient float64 `json:"effortCoefficient"` } type atomizeRequest struct { TaskID string `json:"taskId"` Title string `json:"title"` Description string `json:"description"` AcceptanceCriteria []string `json:"acceptanceCriteria"` Attachments []struct { Name string `json:"name"` URL string `json:"url"` MimeType string `json:"mimeType"` } `json:"attachments"` Links []string `json:"links"` SubdivisionNote string `json:"subdivisionNote"` ExtensionNote string `json:"extensionNote"` Constraints *struct { MinTasks int `json:"minTasks"` MaxTasks int `json:"maxTasks"` } `json:"constraints"` } type server struct { cfg config log *slog.Logger llm *llmClient } func main() { cfg := loadConfig() log := slog.New(slog.NewJSONHandler(os.Stdout, nil)) s := &server{cfg: cfg, log: log, llm: newLLMClient(cfg, log)} mux := http.NewServeMux() mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) }) mux.HandleFunc("POST /v1/atomize", s.auth(s.handleAtomize)) mux.HandleFunc("POST /v1/extend", s.auth(s.handleExtend)) srv := &http.Server{Addr: ":" + cfg.port, Handler: mux, ReadHeaderTimeout: 10 * time.Second} ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() go func() { log.Info("atomizer-mock listening", "port", cfg.port, "llm", map[bool]string{true: "anthropic:" + cfg.apiStyle, false: "fallback (no api key)"}[cfg.apiKey != ""]) if err := srv.ListenAndServe(); err != http.ErrServerClosed { log.Error("serve", "err", err) os.Exit(1) } }() <-ctx.Done() shutCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() srv.Shutdown(shutCtx) } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(v) } func writeErr(w http.ResponseWriter, status int, code, msg string) { writeJSON(w, status, map[string]any{"error": map[string]string{"code": code, "message": msg}}) } func (s *server) auth(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if s.cfg.token != "" && r.Header.Get("Authorization") != "Bearer "+s.cfg.token { writeErr(w, http.StatusUnauthorized, "unauthorized", "missing or invalid bearer token") return } next(w, r) } } func (s *server) handleAtomize(w http.ResponseWriter, r *http.Request) { var req atomizeRequest if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<20)).Decode(&req); err != nil { writeErr(w, http.StatusBadRequest, "bad_request", err.Error()) return } minT, maxT := 2, 8 if req.Constraints != nil { if req.Constraints.MinTasks > 0 { minT = req.Constraints.MinTasks } if req.Constraints.MaxTasks > 0 { maxT = req.Constraints.MaxTasks } } if minT > maxT { minT = maxT } tasks, model, notes, err := s.llm.atomize(r.Context(), req, minT, maxT) if err != nil { s.log.Error("atomize failed", "taskId", req.TaskID, "err", err) writeErr(w, http.StatusBadGateway, "llm_failed", err.Error()) return } normalizeSum(tasks) writeJSON(w, http.StatusOK, map[string]any{"tasks": tasks, "model": model, "notes": notes}) } func (s *server) handleExtend(w http.ResponseWriter, r *http.Request) { var req atomizeRequest if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<20)).Decode(&req); err != nil { writeErr(w, http.StatusBadRequest, "bad_request", err.Error()) return } if strings.TrimSpace(req.ExtensionNote) == "" { writeErr(w, http.StatusBadRequest, "note_required", "extensionNote is required") return } task, model, notes, err := s.llm.extend(r.Context(), req) if err != nil { s.log.Error("extend failed", "taskId", req.TaskID, "err", err) writeErr(w, http.StatusBadGateway, "llm_failed", err.Error()) return } if task.EffortCoefficient <= 0 || task.EffortCoefficient > 2 { task.EffortCoefficient = 0.3 } writeJSON(w, http.StatusOK, map[string]any{"task": task, "model": model, "notes": notes}) } // normalizeSum forces coefficients to sum to exactly 1.0. func normalizeSum(tasks []subTask) { sum := 0.0 for _, t := range tasks { sum += t.EffortCoefficient } if sum <= 0 { eq := 1.0 / float64(len(tasks)) for i := range tasks { tasks[i].EffortCoefficient = eq } sum = 1.0 } total := 0.0 largest := 0 for i := range tasks { tasks[i].EffortCoefficient = round4(tasks[i].EffortCoefficient / sum) total += tasks[i].EffortCoefficient if tasks[i].EffortCoefficient > tasks[largest].EffortCoefficient { largest = i } } tasks[largest].EffortCoefficient = round4(tasks[largest].EffortCoefficient + 1 - total) } func round4(v float64) float64 { return float64(int64(v*10000+0.5)) / 10000 } func fallbackAtomize(req atomizeRequest, minT, maxT int) []subTask { n := (minT + maxT) / 2 if n < 1 { n = 3 } out := make([]subTask, n) eq := round4(1.0 / float64(n)) for i := range out { out[i] = subTask{ Title: fmt.Sprintf("%s — part %d of %d", req.Title, i+1, n), Description: fmt.Sprintf("Deterministic offline split %d/%d of:\n\n%s", i+1, n, req.Description), AcceptanceCriteria: append([]string{}, req.AcceptanceCriteria...), EffortCoefficient: eq, } } return out } func fallbackExtend(req atomizeRequest) subTask { return subTask{ Title: req.Title + " — extension: " + firstLine(req.ExtensionNote, 60), Description: "Deterministic offline extension of the source task.\n\nRequested scope: " + req.ExtensionNote, AcceptanceCriteria: []string{"extension scope implemented: " + firstLine(req.ExtensionNote, 120)}, EffortCoefficient: 0.3, } } func firstLine(s string, max int) string { if i := strings.IndexByte(s, '\n'); i >= 0 { s = s[:i] } if len(s) > max { s = s[:max] } return s }