From 70e90275c0a08649c314cae5280521bcd29272e6 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Sat, 4 Apr 2026 09:36:56 +0000 Subject: feat: acceptance_criteria per story task in elaboration and approval Add AcceptanceCriteria field to elaboratedStoryTask struct, update buildStoryElaboratePrompt schema and rules, pass the value through handleApproveStory into task.Task, and add a test verifying the field is persisted on approved story tasks. Co-Authored-By: Claude Sonnet 4.6 --- internal/api/elaborate.go | 11 ++++++---- internal/api/stories.go | 27 ++++++++++++------------ internal/api/stories_test.go | 49 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 17 deletions(-) diff --git a/internal/api/elaborate.go b/internal/api/elaborate.go index dd51c7d..0cb298d 100644 --- a/internal/api/elaborate.go +++ b/internal/api/elaborate.go @@ -282,9 +282,10 @@ type elaboratedStorySubtask struct { // elaboratedStoryTask is one independently-deployable unit in a story plan. type elaboratedStoryTask struct { - Name string `json:"name"` - Instructions string `json:"instructions"` - Subtasks []elaboratedStorySubtask `json:"subtasks"` + Name string `json:"name"` + Instructions string `json:"instructions"` + AcceptanceCriteria string `json:"acceptance_criteria"` + Subtasks []elaboratedStorySubtask `json:"subtasks"` } // elaboratedStoryValidation describes how to verify the story was successful. @@ -313,6 +314,7 @@ Output ONLY valid JSON matching this schema: { "name": "task name", "instructions": "detailed instructions including file paths and what to change", + "acceptance_criteria": "specific, verifiable conditions a separate reviewer can check — e.g. 'run go test ./... and verify all pass; confirm GET /api/foo returns 200 with expected JSON shape'", "subtasks": [ { "name": "subtask name", "instructions": "..." } ] @@ -330,7 +332,8 @@ Rules: - Subtasks within a task are order-dependent and run sequentially - Instructions must include specific file paths, function names, and exact changes - Instructions must end with: git add -A && git commit -m "..." && git push origin -- Validation should match the scope: small change = build check; new feature = smoke test` +- Validation should match the scope: small change = build check; new feature = smoke test +- acceptance_criteria must be concrete and verifiable by a separate agent — no vague assertions like "code looks good"` } func (s *Server) elaborateStoryWithClaude(ctx context.Context, workDir, goal string) (*elaboratedStory, error) { diff --git a/internal/api/stories.go b/internal/api/stories.go index 1743dbe..fa10ccd 100644 --- a/internal/api/stories.go +++ b/internal/api/stories.go @@ -254,19 +254,20 @@ func (s *Server) handleApproveStory(w http.ResponseWriter, r *http.Request) { 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}, - 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}, + 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} diff --git a/internal/api/stories_test.go b/internal/api/stories_test.go index 342840b..f43ad86 100644 --- a/internal/api/stories_test.go +++ b/internal/api/stories_test.go @@ -258,6 +258,55 @@ func TestHandleStoryApprove_SetsRepositoryURL(t *testing.T) { } } +func TestApproveStory_AcceptanceCriteriaStored(t *testing.T) { + srv, store := testServer(t) + + proj := &task.Project{ + ID: "ac-proj", Name: "test", RemoteURL: "https://github.com/x/y", + Type: "web", DeployScript: "", + } + store.CreateProject(proj) + + body := `{ + "name": "AC Story", + "branch_name": "story/ac-test", + "project_id": "ac-proj", + "tasks": [ + { + "name": "Add feature", + "instructions": "implement the thing", + "acceptance_criteria": "run go test ./... and verify all pass", + "subtasks": [] + } + ], + "validation": {"type": "test", "steps": [], "success_criteria": "tests pass"} + }` + req := httptest.NewRequest("POST", "/api/stories/approve", strings.NewReader(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"` + } + json.NewDecoder(w.Body).Decode(&resp) + if len(resp.TaskIDs) == 0 { + t.Fatal("expected task_ids in response") + } + + tk, err := store.GetTask(resp.TaskIDs[0]) + if err != nil { + t.Fatalf("GetTask: %v", err) + } + if tk.AcceptanceCriteria != "run go test ./... and verify all pass" { + t.Errorf("expected acceptance criteria stored on task, got %q", tk.AcceptanceCriteria) + } +} + func TestHandleStoryDeploymentStatus(t *testing.T) { srv, store := testServer(t) -- cgit v1.2.3