From 5410069ae36bc5df5d7cc950fce5d2c5a251618a Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Thu, 26 Mar 2026 04:57:37 +0000 Subject: fix: set RepositoryURL on tasks created via story approve handleApproveStory was creating tasks without RepositoryURL, causing executions to fail with "task has no repository_url". Now looks up the project's RemoteURL and sets it on all tasks and subtasks. Co-Authored-By: Claude Sonnet 4.6 --- internal/api/stories.go | 59 +++++++++++++++++++++++++------------------- internal/api/stories_test.go | 52 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 25 deletions(-) diff --git a/internal/api/stories.go b/internal/api/stories.go index fcf8c48..3d9d25a 100644 --- a/internal/api/stories.go +++ b/internal/api/stories.go @@ -247,22 +247,30 @@ func (s *Server) handleApproveStory(w http.ResponseWriter, r *http.Request) { 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, - 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(), + ID: uuid.New().String(), + Name: tp.Name, + Project: input.ProjectID, + RepositoryURL: repoURL, + 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} @@ -276,19 +284,20 @@ func (s *Server) handleApproveStory(w http.ResponseWriter, r *http.Request) { var prevSubtaskID string for _, sub := range tp.Subtasks { st := &task.Task{ - ID: uuid.New().String(), - Name: sub.Name, - Project: input.ProjectID, - 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(), + 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} diff --git a/internal/api/stories_test.go b/internal/api/stories_test.go index 17bea07..53d15eb 100644 --- a/internal/api/stories_test.go +++ b/internal/api/stories_test.go @@ -205,6 +205,58 @@ func TestHandleStoryApprove_WiresDepends(t *testing.T) { } } +func TestHandleStoryApprove_SetsRepositoryURL(t *testing.T) { + srv, store := testServer(t) + + proj := &task.Project{ + ID: "proj-repo", + Name: "claudomator", + RemoteURL: "/site/git.terst.org/repos/claudomator.git", + LocalPath: "/workspace/claudomator", + } + if err := store.CreateProject(proj); err != nil { + t.Fatalf("CreateProject: %v", err) + } + + body := `{ + "name": "Repo URL Story", + "branch_name": "story/repo-url", + "project_id": "proj-repo", + "tasks": [ + {"name": "Task A", "instructions": "do A", "subtasks": []}, + {"name": "Task B", "instructions": "do B", "subtasks": [ + {"name": "Sub B1", "instructions": "do B1"} + ]} + ], + "validation": {"type": "build", "steps": ["go build ./..."], "success_criteria": "ok"} + }` + req := httptest.NewRequest("POST", "/api/stories/approve", bytes.NewBufferString(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + srv.mux.ServeHTTP(w, req) + + if w.Code != http.StatusCreated { + t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String()) + } + + var resp struct { + TaskIDs []string `json:"task_ids"` + } + if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { + t.Fatalf("decode: %v", err) + } + + for _, id := range resp.TaskIDs { + tk, err := store.GetTask(id) + if err != nil { + t.Fatalf("GetTask %s: %v", id, err) + } + if tk.RepositoryURL != proj.RemoteURL { + t.Errorf("task %s RepositoryURL: want %q, got %q", tk.ID, proj.RemoteURL, tk.RepositoryURL) + } + } +} + func TestHandleStoryDeploymentStatus(t *testing.T) { srv, store := testServer(t) -- cgit v1.2.3