package api import ( "encoding/json" "net/http" "time" "github.com/google/uuid" "github.com/thepeterstone/claudomator/internal/task" ) func (s *Server) handleListStories(w http.ResponseWriter, r *http.Request) { stories, err := s.store.ListStories() if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } if stories == nil { stories = []*task.Story{} } writeJSON(w, http.StatusOK, stories) } func (s *Server) handleCreateStory(w http.ResponseWriter, r *http.Request) { var st task.Story if err := json.NewDecoder(r.Body).Decode(&st); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()}) return } if st.Name == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"}) return } if st.ID == "" { st.ID = uuid.New().String() } if st.Status == "" { st.Status = task.StoryPending } if err := s.store.CreateStory(&st); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusCreated, st) } func (s *Server) handleGetStory(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") st, err := s.store.GetStory(id) if err != nil { writeJSON(w, http.StatusNotFound, map[string]string{"error": "story not found"}) return } writeJSON(w, http.StatusOK, st) } func (s *Server) handleListStoryTasks(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if _, err := s.store.GetStory(id); err != nil { writeJSON(w, http.StatusNotFound, map[string]string{"error": "story not found"}) return } tasks, err := s.store.ListTasksByStory(id) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } if tasks == nil { tasks = []*task.Task{} } writeJSON(w, http.StatusOK, tasks) } func (s *Server) handleAddTaskToStory(w http.ResponseWriter, r *http.Request) { storyID := r.PathValue("id") st, err := s.store.GetStory(storyID) if err != nil { writeJSON(w, http.StatusNotFound, map[string]string{"error": "story not found"}) return } _ = st var input struct { Name string `json:"name"` Description string `json:"description"` Project string `json:"project"` RepositoryURL string `json:"repository_url"` Agent task.AgentConfig `json:"agent"` Claude task.AgentConfig `json:"claude"` Timeout string `json:"timeout"` Priority string `json:"priority"` Tags []string `json:"tags"` ParentTaskID string `json:"parent_task_id"` } if err := json.NewDecoder(r.Body).Decode(&input); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()}) return } if input.Agent.Instructions == "" && input.Claude.Instructions != "" { input.Agent = input.Claude } existing, err := s.store.ListTasksByStory(storyID) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } now := time.Now().UTC() t := &task.Task{ ID: uuid.New().String(), Name: input.Name, Description: input.Description, Project: input.Project, RepositoryURL: input.RepositoryURL, Agent: input.Agent, Priority: task.Priority(input.Priority), Tags: input.Tags, DependsOn: []string{}, Retry: task.RetryConfig{MaxAttempts: 1, Backoff: "exponential"}, State: task.StatePending, StoryID: storyID, ParentTaskID: input.ParentTaskID, CreatedAt: now, UpdatedAt: now, } if t.Agent.Type == "" { t.Agent.Type = "claude" } if t.Priority == "" { t.Priority = task.PriorityNormal } if t.Tags == nil { t.Tags = []string{} } if input.Timeout != "" { dur, err := time.ParseDuration(input.Timeout) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid timeout: " + err.Error()}) return } t.Timeout.Duration = dur } // Auto-wire depends_on: new task depends on the last existing task (sorted ASC by created_at). if len(existing) > 0 { lastTask := existing[len(existing)-1] t.DependsOn = []string{lastTask.ID} } if err := s.store.CreateTask(t); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusCreated, t) } func (s *Server) handleUpdateStoryStatus(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") st, err := s.store.GetStory(id) if err != nil { writeJSON(w, http.StatusNotFound, map[string]string{"error": "story not found"}) return } var input struct { Status task.StoryState `json:"status"` } if err := json.NewDecoder(r.Body).Decode(&input); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()}) return } if !task.ValidStoryTransition(st.Status, input.Status) { writeJSON(w, http.StatusConflict, map[string]string{ "error": "invalid story status transition from " + string(st.Status) + " to " + string(input.Status), }) return } if err := s.store.UpdateStoryStatus(id, input.Status); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]string{"message": "story status updated", "story_id": id, "status": string(input.Status)}) }