summaryrefslogtreecommitdiff
path: root/internal/api/stories.go
diff options
context:
space:
mode:
authorClaudomator Agent <agent@claudomator>2026-06-05 09:21:32 +0000
committerClaudomator Agent <agent@claudomator>2026-06-05 09:21:32 +0000
commit0eb0b79396663c4901597becc6857a4cd795c58e (patch)
tree3532c82aa3f7c6deeed03f143d1c360d7cf6cc47 /internal/api/stories.go
parentfa3ed5220a28f326d443726233c37e1a7df0ced5 (diff)
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 <noreply@anthropic.com>
Diffstat (limited to 'internal/api/stories.go')
-rw-r--r--internal/api/stories.go378
1 files changed, 0 insertions, 378 deletions
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)
-}