diff options
Diffstat (limited to 'internal/api/stories.go')
| -rw-r--r-- | internal/api/stories.go | 185 |
1 files changed, 185 insertions, 0 deletions
diff --git a/internal/api/stories.go b/internal/api/stories.go new file mode 100644 index 0000000..4b91653 --- /dev/null +++ b/internal/api/stories.go @@ -0,0 +1,185 @@ +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)}) +} |
