diff options
Diffstat (limited to 'internal/task')
| -rw-r--r-- | internal/task/project.go | 11 | ||||
| -rw-r--r-- | internal/task/story.go | 41 | ||||
| -rw-r--r-- | internal/task/story_test.go | 42 | ||||
| -rw-r--r-- | internal/task/task.go | 12 | ||||
| -rw-r--r-- | internal/task/task_test.go | 28 | ||||
| -rw-r--r-- | internal/task/validator.go | 3 | ||||
| -rw-r--r-- | internal/task/validator_test.go | 2 |
7 files changed, 136 insertions, 3 deletions
diff --git a/internal/task/project.go b/internal/task/project.go new file mode 100644 index 0000000..bd8a4fb --- /dev/null +++ b/internal/task/project.go @@ -0,0 +1,11 @@ +package task + +// Project represents a registered codebase that agents can operate on. +type Project struct { + ID string `json:"id"` + Name string `json:"name"` + RemoteURL string `json:"remote_url"` + LocalPath string `json:"local_path"` + Type string `json:"type"` // "web" | "android" + DeployScript string `json:"deploy_script"` // optional path or command +} diff --git a/internal/task/story.go b/internal/task/story.go new file mode 100644 index 0000000..536bda1 --- /dev/null +++ b/internal/task/story.go @@ -0,0 +1,41 @@ +package task + +import "time" + +type StoryState string + +const ( + StoryPending StoryState = "PENDING" + StoryInProgress StoryState = "IN_PROGRESS" + StoryShippable StoryState = "SHIPPABLE" + StoryDeployed StoryState = "DEPLOYED" + StoryValidating StoryState = "VALIDATING" + StoryReviewReady StoryState = "REVIEW_READY" + StoryNeedsFix StoryState = "NEEDS_FIX" +) + +type Story struct { + ID string `json:"id"` + Name string `json:"name"` + ProjectID string `json:"project_id"` + BranchName string `json:"branch_name"` + DeployConfig string `json:"deploy_config"` + ValidationJSON string `json:"validation_json"` + Status StoryState `json:"status"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +var validStoryTransitions = map[StoryState]map[StoryState]bool{ + StoryPending: {StoryInProgress: true}, + StoryInProgress: {StoryShippable: true, StoryNeedsFix: true}, + StoryShippable: {StoryDeployed: true}, + StoryDeployed: {StoryValidating: true}, + StoryValidating: {StoryReviewReady: true, StoryNeedsFix: true}, + StoryReviewReady: {}, + StoryNeedsFix: {StoryInProgress: true}, +} + +func ValidStoryTransition(from, to StoryState) bool { + return validStoryTransitions[from][to] +} diff --git a/internal/task/story_test.go b/internal/task/story_test.go new file mode 100644 index 0000000..38d0290 --- /dev/null +++ b/internal/task/story_test.go @@ -0,0 +1,42 @@ +package task + +import "testing" + +func TestValidStoryTransition_Valid(t *testing.T) { + cases := []struct { + from StoryState + to StoryState + }{ + {StoryPending, StoryInProgress}, + {StoryInProgress, StoryShippable}, + {StoryInProgress, StoryNeedsFix}, + {StoryNeedsFix, StoryInProgress}, + {StoryShippable, StoryDeployed}, + {StoryDeployed, StoryValidating}, + {StoryValidating, StoryReviewReady}, + {StoryValidating, StoryNeedsFix}, + } + for _, tc := range cases { + if !ValidStoryTransition(tc.from, tc.to) { + t.Errorf("expected valid transition %s → %s", tc.from, tc.to) + } + } +} + +func TestValidStoryTransition_Invalid(t *testing.T) { + cases := []struct { + from StoryState + to StoryState + }{ + {StoryPending, StoryDeployed}, + {StoryReviewReady, StoryPending}, + {StoryReviewReady, StoryInProgress}, + {StoryReviewReady, StoryShippable}, + {StoryShippable, StoryPending}, + } + for _, tc := range cases { + if ValidStoryTransition(tc.from, tc.to) { + t.Errorf("expected invalid transition %s → %s", tc.from, tc.to) + } + } +} diff --git a/internal/task/task.go b/internal/task/task.go index fd1dde6..935a238 100644 --- a/internal/task/task.go +++ b/internal/task/task.go @@ -32,13 +32,14 @@ type AgentConfig struct { Model string `yaml:"model" json:"model"` ContextFiles []string `yaml:"context_files" json:"context_files"` Instructions string `yaml:"instructions" json:"instructions"` - ProjectDir string `yaml:"project_dir" json:"project_dir"` + ContainerImage string `yaml:"container_image" json:"container_image"` MaxBudgetUSD float64 `yaml:"max_budget_usd" json:"max_budget_usd"` PermissionMode string `yaml:"permission_mode" json:"permission_mode"` AllowedTools []string `yaml:"allowed_tools" json:"allowed_tools"` DisallowedTools []string `yaml:"disallowed_tools" json:"disallowed_tools"` SystemPromptAppend string `yaml:"system_prompt_append" json:"system_prompt_append"` AdditionalArgs []string `yaml:"additional_args" json:"additional_args"` + ProjectDir string `yaml:"project_dir" json:"project_dir,omitempty"` SkipPlanning bool `yaml:"skip_planning" json:"skip_planning"` // Local-runner sampling controls. Pointer for Temperature so a 0 value can @@ -79,12 +80,19 @@ type Task struct { ParentTaskID string `yaml:"parent_task_id" json:"parent_task_id"` Name string `yaml:"name" json:"name"` Description string `yaml:"description" json:"description"` + Project string `yaml:"project" json:"project"` // Human-readable project name + RepositoryURL string `yaml:"repository_url" json:"repository_url"` Agent AgentConfig `yaml:"agent" json:"agent"` Timeout Duration `yaml:"timeout" json:"timeout"` Retry RetryConfig `yaml:"retry" json:"retry"` Priority Priority `yaml:"priority" json:"priority"` Tags []string `yaml:"tags" json:"tags"` DependsOn []string `yaml:"depends_on" json:"depends_on"` + StoryID string `yaml:"-" json:"story_id,omitempty"` + BranchName string `yaml:"-" json:"branch_name,omitempty"` + AcceptanceCriteria string `yaml:"-" json:"acceptance_criteria,omitempty"` + CheckerForTaskID string `yaml:"-" json:"checker_for_task_id,omitempty"` + CheckerReport string `yaml:"-" json:"checker_report,omitempty"` State State `yaml:"-" json:"state"` RejectionComment string `yaml:"-" json:"rejection_comment,omitempty"` QuestionJSON string `yaml:"-" json:"question,omitempty"` @@ -130,7 +138,7 @@ type BatchFile struct { // BLOCKED may advance to READY when all subtasks complete, or back to QUEUED on user answer. var validTransitions = map[State]map[State]bool{ StatePending: {StateQueued: true, StateCancelled: true}, - StateQueued: {StateRunning: true, StateCancelled: true}, + StateQueued: {StateRunning: true, StateCancelled: true, StateReady: true}, // READY: parent task completed via subtask delegation StateRunning: {StateReady: true, StateCompleted: true, StateFailed: true, StateTimedOut: true, StateCancelled: true, StateBudgetExceeded: true, StateBlocked: true}, StateReady: {StateCompleted: true, StatePending: true}, StateFailed: {StateQueued: true}, // retry diff --git a/internal/task/task_test.go b/internal/task/task_test.go index 15ba019..e6a17b8 100644 --- a/internal/task/task_test.go +++ b/internal/task/task_test.go @@ -100,3 +100,31 @@ func TestDuration_MarshalYAML(t *testing.T) { t.Errorf("expected '15m0s', got %v", v) } } + +func TestTask_ProjectField(t *testing.T) { + t.Run("struct assignment", func(t *testing.T) { + task := Task{Project: "my-project"} + if task.Project != "my-project" { + t.Errorf("expected Project 'my-project', got %q", task.Project) + } + }) + + t.Run("yaml parsing", func(t *testing.T) { + yaml := ` +name: "Test Task" +project: my-project +agent: + instructions: "Do something" +` + tasks, err := Parse([]byte(yaml)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(tasks) != 1 { + t.Fatalf("expected 1 task, got %d", len(tasks)) + } + if tasks[0].Project != "my-project" { + t.Errorf("expected Project 'my-project', got %q", tasks[0].Project) + } + }) +} diff --git a/internal/task/validator.go b/internal/task/validator.go index 003fab9..43e482e 100644 --- a/internal/task/validator.go +++ b/internal/task/validator.go @@ -29,6 +29,9 @@ func Validate(t *Task) error { if t.Name == "" { ve.Add("name is required") } + if t.RepositoryURL == "" { + ve.Add("repository_url is required") + } if t.Agent.Instructions == "" { ve.Add("agent.instructions is required") } diff --git a/internal/task/validator_test.go b/internal/task/validator_test.go index 657d93f..2c6735c 100644 --- a/internal/task/validator_test.go +++ b/internal/task/validator_test.go @@ -9,10 +9,10 @@ func validTask() *Task { return &Task{ ID: "test-id", Name: "Valid Task", + RepositoryURL: "https://github.com/user/repo", Agent: AgentConfig{ Type: "claude", Instructions: "do something", - ProjectDir: "/tmp", }, Priority: PriorityNormal, Retry: RetryConfig{MaxAttempts: 1, Backoff: "exponential"}, |
