summaryrefslogtreecommitdiff
path: root/internal/api/stories.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api/stories.go')
-rw-r--r--internal/api/stories.go123
1 files changed, 123 insertions, 0 deletions
diff --git a/internal/api/stories.go b/internal/api/stories.go
index 4b91653..459d0db 100644
--- a/internal/api/stories.go
+++ b/internal/api/stories.go
@@ -2,13 +2,34 @@ package api
import (
"encoding/json"
+ "fmt"
"net/http"
+ "os/exec"
+ "strings"
"time"
"github.com/google/uuid"
"github.com/thepeterstone/claudomator/internal/task"
)
+// createStoryBranch creates a new git branch in localPath and pushes it to origin.
+func createStoryBranch(localPath, branchName string) error {
+ out, err := exec.Command("git", "-C", localPath, "checkout", "-b", branchName).CombinedOutput()
+ if err != nil {
+ if !strings.Contains(string(out), "already exists") {
+ return fmt.Errorf("git checkout -b: %w (output: %s)", err, string(out))
+ }
+ // Branch exists; switch to it.
+ if out2, err2 := exec.Command("git", "-C", localPath, "checkout", branchName).CombinedOutput(); err2 != nil {
+ return fmt.Errorf("git checkout: %w (output: %s)", err2, string(out2))
+ }
+ }
+ if out, err := exec.Command("git", "-C", localPath, "push", "-u", "origin", 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 {
@@ -183,3 +204,105 @@ func (s *Server) handleUpdateStoryStatus(w http.ResponseWriter, r *http.Request)
}
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
+ }
+
+ 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,
+ StoryID: story.ID,
+ Agent: task.AgentConfig{Type: "claude", Instructions: tp.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 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,
+ 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 != "" {
+ if err := createStoryBranch(proj.LocalPath, input.BranchName); 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,
+ })
+}