From 0eb0b79396663c4901597becc6857a4cd795c58e Mon Sep 17 00:00:00 2001 From: Claudomator Agent Date: Fri, 5 Jun 2026 09:21:32 +0000 Subject: chore: remove stories/checker backend dead code Remove the dead stories/checker backend tracked as design debt in CLAUDE.md: - Delete internal/api/stories.go, stories_test.go, elaborate.go - Delete internal/task/story.go, story_test.go - Remove story/checker columns from storage schema and all CRUD methods - Remove checkStoryCompletion, spawnCheckerTask, triggerStoryDeploy, createValidationTask, checkValidationResult, ShipStory, CheckStoryCompletion from executor; simplify handleRunResult - Remove story/checker fields from task.Task (StoryID, AcceptanceCriteria, CheckerForTaskID, CheckerReport) - Remove story routes and geminiBinPath from API server - Move claudeJSONResult/extractJSON from elaborate.go to validate.go - Rename ensureStoryBranch -> ensureBranch in ContainerRunner - Fix git identity in bare-repo tests; fix initial-branch to use main Co-Authored-By: Claude Sonnet 4.6 --- internal/api/stories.go | 378 ------------------------------------------------ 1 file changed, 378 deletions(-) delete mode 100644 internal/api/stories.go (limited to 'internal/api/stories.go') diff --git a/internal/api/stories.go b/internal/api/stories.go deleted file mode 100644 index fa10ccd..0000000 --- a/internal/api/stories.go +++ /dev/null @@ -1,378 +0,0 @@ -package api - -import ( - "database/sql" - "encoding/json" - "fmt" - "net/http" - "os/exec" - "strings" - "time" - - "github.com/google/uuid" - "github.com/thepeterstone/claudomator/internal/deployment" - "github.com/thepeterstone/claudomator/internal/task" -) - -// createStoryBranch creates a new git branch in localPath from the latest main -// and pushes it to remoteURL (the bare repo). Idempotent: treats "already exists" as success. -func createStoryBranch(localPath, branchName, remoteURL string) error { - // Fetch latest from the bare repo so we have an up-to-date base. - if out, err := exec.Command("git", "-C", localPath, "fetch", remoteURL, "main").CombinedOutput(); err != nil { - return fmt.Errorf("git fetch: %w (output: %s)", err, string(out)) - } - base := "FETCH_HEAD" - out, err := exec.Command("git", "-C", localPath, "checkout", "-b", branchName, base).CombinedOutput() - if err != nil { - if !strings.Contains(string(out), "already exists") { - return fmt.Errorf("git checkout -b: %w (output: %s)", err, string(out)) - } - } - if out, err := exec.Command("git", "-C", localPath, "push", remoteURL, branchName).CombinedOutput(); err != nil { - return fmt.Errorf("git push: %w (output: %s)", err, string(out)) - } - return nil -} - -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)}) -} - -func (s *Server) handleApproveStory(w http.ResponseWriter, r *http.Request) { - var input struct { - Name string `json:"name"` - BranchName string `json:"branch_name"` - ProjectID string `json:"project_id"` - Tasks []elaboratedStoryTask `json:"tasks"` - Validation elaboratedStoryValidation `json:"validation"` - } - 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.Name == "" { - writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"}) - return - } - - validationJSON, _ := json.Marshal(input.Validation) - now := time.Now().UTC() - story := &task.Story{ - ID: uuid.New().String(), - Name: input.Name, - ProjectID: input.ProjectID, - BranchName: input.BranchName, - ValidationJSON: string(validationJSON), - Status: task.StoryPending, - CreatedAt: now, - UpdatedAt: now, - } - if err := s.store.CreateStory(story); err != nil { - writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) - return - } - - var repoURL string - if input.ProjectID != "" { - if proj, err := s.store.GetProject(input.ProjectID); err == nil { - repoURL = proj.RemoteURL - } - } - - taskIDs := make([]string, 0, len(input.Tasks)) - var prevTaskID string - for _, tp := range input.Tasks { - t := &task.Task{ - ID: uuid.New().String(), - Name: tp.Name, - Project: input.ProjectID, - RepositoryURL: repoURL, - StoryID: story.ID, - Agent: task.AgentConfig{Type: "claude", Instructions: tp.Instructions}, - AcceptanceCriteria: tp.AcceptanceCriteria, - Priority: task.PriorityNormal, - Tags: []string{}, - DependsOn: []string{}, - Retry: task.RetryConfig{MaxAttempts: 1, Backoff: "exponential"}, - State: task.StatePending, - CreatedAt: time.Now().UTC(), - UpdatedAt: time.Now().UTC(), - } - if prevTaskID != "" { - t.DependsOn = []string{prevTaskID} - } - if err := s.store.CreateTask(t); err != nil { - writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) - return - } - taskIDs = append(taskIDs, t.ID) - - var prevSubtaskID string - for _, sub := range tp.Subtasks { - st := &task.Task{ - ID: uuid.New().String(), - Name: sub.Name, - Project: input.ProjectID, - RepositoryURL: repoURL, - StoryID: story.ID, - ParentTaskID: t.ID, - Agent: task.AgentConfig{Type: "claude", Instructions: sub.Instructions}, - Priority: task.PriorityNormal, - Tags: []string{}, - DependsOn: []string{}, - Retry: task.RetryConfig{MaxAttempts: 1, Backoff: "exponential"}, - State: task.StatePending, - CreatedAt: time.Now().UTC(), - UpdatedAt: time.Now().UTC(), - } - if prevSubtaskID != "" { - st.DependsOn = []string{prevSubtaskID} - } - if err := s.store.CreateTask(st); err != nil { - writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) - return - } - prevSubtaskID = st.ID - } - prevTaskID = t.ID - } - - // Create the story branch (non-fatal if it fails). - if input.BranchName != "" && input.ProjectID != "" { - if proj, err := s.store.GetProject(input.ProjectID); err == nil && proj.LocalPath != "" && proj.RemoteURL != "" { - if err := createStoryBranch(proj.LocalPath, input.BranchName, proj.RemoteURL); err != nil { - s.logger.Warn("story approve: failed to create branch", "error", err, "branch", input.BranchName) - } - } - } - - writeJSON(w, http.StatusCreated, map[string]interface{}{ - "story": story, - "task_ids": taskIDs, - }) -} - -// handleShipStory triggers the merge + deploy for a SHIPPABLE story. -// POST /api/stories/{id}/ship -func (s *Server) handleShipStory(w http.ResponseWriter, r *http.Request) { - id := r.PathValue("id") - if err := s.pool.ShipStory(r.Context(), id); err != nil { - writeJSON(w, http.StatusConflict, map[string]string{"error": err.Error()}) - return - } - writeJSON(w, http.StatusAccepted, map[string]string{"message": "story shipping initiated", "story_id": id}) -} - -// handleStoryDeploymentStatus aggregates the deployment status across all tasks in a story. -// GET /api/stories/{id}/deployment-status -func (s *Server) handleStoryDeploymentStatus(w http.ResponseWriter, r *http.Request) { - id := r.PathValue("id") - - story, err := s.store.GetStory(id) - if 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 - } - - // Collect all commits from the latest execution of each task. - var allCommits []task.GitCommit - for _, t := range tasks { - exec, err := s.store.GetLatestExecution(t.ID) - if err != nil { - if err == sql.ErrNoRows { - continue - } - writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) - return - } - allCommits = append(allCommits, exec.Commits...) - } - - // Determine project remote URL for the deployment check. - projectRemoteURL := "" - if story.ProjectID != "" { - if proj, err := s.store.GetProject(story.ProjectID); err == nil { - projectRemoteURL = proj.RemoteURL - } - } - - status := deployment.Check(allCommits, projectRemoteURL) - writeJSON(w, http.StatusOK, status) -} -- cgit v1.2.3